Nav2 Navigation Stack - kilted  kilted
ROS 2 Navigation Stack
generate_motion_primitives.py
1 # Copyright (c) 2021, Matthew Booker
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License. Reserved.
14 
15 import argparse
16 from datetime import datetime
17 import json
18 import logging
19 from pathlib import Path
20 import time
21 from typing import Any, cast, Dict, List, TypedDict
22 
23 import matplotlib.pyplot as plt
24 from nav2_smac_planner.lattice_primitives import constants
25 from nav2_smac_planner.lattice_primitives.lattice_generator import ConfigDict, LatticeGenerator
26 from nav2_smac_planner.lattice_primitives.trajectory import Trajectory
27 import numpy as np
28 
29 logging.basicConfig(level=logging.INFO)
30 logger = logging.getLogger(__name__)
31 
32 
33 class HeaderDict(TypedDict):
34  version: float
35  date_generated: str
36  lattice_metadata: Dict[str, Any]
37  primitives: List[Dict[str, Any]]
38 
39 
40 def handle_arg_parsing() -> argparse.Namespace:
41  """
42  Handle the parsing of arguments.
43 
44  Returns
45  -------
46  argparse.Namespace
47  An object containing all parsed arguments
48 
49  """
50  parser = argparse.ArgumentParser(
51  description="Generate motionprimitives for Nav2's State Lattice Planner"
52  )
53  parser.add_argument(
54  '--config',
55  type=Path,
56  default='./config.json',
57  help='The config file containing the ' 'parameters to be used',
58  )
59  parser.add_argument(
60  '--output',
61  type=Path,
62  default='./output.json',
63  help='The output file containing the ' 'trajectory data',
64  )
65  parser.add_argument(
66  '--visualizations',
67  type=Path,
68  default='./visualizations',
69  help='The output folder where the '
70  'visualizations of the trajectories will be saved',
71  )
72 
73  return parser.parse_args()
74 
75 
76 def create_heading_angle_list(minimal_set_trajectories: Dict[float, List[Trajectory]]
77  ) -> List[float]:
78  """
79  Create a sorted list of heading angles from the minimal trajectory set.
80 
81  Args:
82  ----
83  minimal_set_trajectories: dict
84  The minimal spanning set
85 
86  Returns
87  -------
88  list
89  A sorted list of heading angles
90 
91  """
92  heading_angles = set(minimal_set_trajectories.keys())
93  return sorted(heading_angles, key=lambda x: (x < 0, x))
94 
95 
96 def read_config(config_path: Path) -> ConfigDict:
97  """
98  Read in the user defined parameters via JSON.
99 
100  Args:
101  ----
102  config_path: Path
103  Path to the config file
104 
105  Returns
106  -------
107  dict
108  Dictionary containing the user defined parameters
109 
110  """
111  with open(config_path) as config_file:
112  config = json.load(config_file)
113 
114  return cast(ConfigDict, config)
115 
116 
117 def create_header(config: ConfigDict, minimal_set_trajectories: Dict[float, List[Trajectory]]
118  ) -> HeaderDict:
119  """
120  Create a dict containing all the fields to populate the header with.
121 
122  Args:
123  ----
124  config: dict
125  The dict containing user specified parameters
126  minimal_set_trajectories: dict
127  The minimal spanning set
128 
129  Returns
130  -------
131  dict
132  A dictionary containing the fields to populate the header with
133 
134  """
135  header_dict: HeaderDict = {
136  'version': constants.VERSION,
137  'date_generated': datetime.today().strftime('%Y-%m-%d'),
138  'lattice_metadata': {},
139  'primitives': [],
140  }
141 
142  for key, value in config.items():
143  header_dict['lattice_metadata'][key] = value
144 
145  heading_angles = create_heading_angle_list(minimal_set_trajectories)
146  adjusted_heading_angles = [
147  angle + 2 * np.pi if angle < 0 else angle for angle in heading_angles
148  ]
149 
150  header_dict['lattice_metadata']['heading_angles'] = adjusted_heading_angles
151 
152  return header_dict
153 
154 
155 def write_to_json(
156  output_path: Path, minimal_set_trajectories: Dict[float, List[Trajectory]], config: ConfigDict
157 ) -> None:
158  """
159  Write the minimal spanning set to an output file.
160 
161  Args:
162  ----
163  output_path: Path
164  The output file for the json data
165  minimal_set_trajectories: dict
166  The minimal spanning set
167  config: dict
168  The dict containing user specified parameters
169 
170  """
171  output_dict = create_header(config, minimal_set_trajectories)
172 
173  trajectory_start_angles = list(minimal_set_trajectories.keys())
174 
175  heading_angle_list = create_heading_angle_list(minimal_set_trajectories)
176  heading_lookup = {angle: idx for idx, angle in enumerate(heading_angle_list)}
177 
178  idx = 0
179  for start_angle in sorted(trajectory_start_angles, key=lambda x: (x < 0, x)):
180 
181  for trajectory in sorted(
182  minimal_set_trajectories[start_angle], key=lambda x: x.parameters.end_angle
183  ):
184 
185  traj_info: Dict[str, Any] = {}
186  traj_info['trajectory_id'] = idx
187  traj_info['start_angle_index'] = heading_lookup[
188  trajectory.parameters.start_angle
189  ]
190  traj_info['end_angle_index'] = heading_lookup[
191  trajectory.parameters.end_angle
192  ]
193  traj_info['left_turn'] = bool(trajectory.parameters.left_turn)
194  traj_info['trajectory_radius'] = trajectory.parameters.turning_radius
195  traj_info['trajectory_length'] = round(
196  trajectory.parameters.total_length, 5
197  )
198  traj_info['arc_length'] = round(trajectory.parameters.arc_length, 5)
199  traj_info['straight_length'] = round(
200  trajectory.parameters.start_straight_length
201  + trajectory.parameters.end_straight_length,
202  5,
203  )
204  traj_info['poses'] = trajectory.path.to_output_format()
205 
206  output_dict['primitives'].append(traj_info)
207  idx += 1
208 
209  output_dict['lattice_metadata']['number_of_trajectories'] = idx
210 
211  with open(output_path, 'w') as output_file:
212  json.dump(output_dict, output_file, indent='\t')
213 
214 
215 def save_visualizations(
216  visualizations_folder: Path, minimal_set_trajectories: Dict[float, List[Trajectory]]
217 ) -> None:
218  """
219  Draw the visualizations for every trajectory and save it as an image.
220 
221  Args:
222  ----
223  visualizations_folder: Path
224  The path to the folder for where to save the images
225  minimal_set_trajectories: dict
226  The minimal spanning set
227 
228  """
229  # Create the directory if it doesn't exist
230  visualizations_folder.mkdir(exist_ok=True)
231 
232  for start_angle in minimal_set_trajectories.keys():
233 
234  for trajectory in minimal_set_trajectories[start_angle]:
235  plt.plot(trajectory.path.xs, trajectory.path.ys, 'b')
236 
237  plt.grid(True)
238  plt.axis('square')
239  left_x, right_x = plt.xlim()
240  left_y, right_y = plt.ylim()
241 
242  output_path = visualizations_folder / 'all_trajectories.png'
243  plt.savefig(output_path)
244  plt.clf()
245 
246  for start_angle in minimal_set_trajectories.keys():
247 
248  angle_in_deg = np.rad2deg(start_angle)
249 
250  if start_angle < 0 or start_angle > np.pi / 2:
251  continue
252 
253  for trajectory in minimal_set_trajectories[start_angle]:
254  plt.plot(trajectory.path.xs, trajectory.path.ys, 'b')
255  plt.xlim(left_x, right_x)
256  plt.ylim(left_y, right_y)
257 
258  plt.grid(True)
259 
260  output_path = visualizations_folder / f'{angle_in_deg}.png'
261  plt.savefig(output_path)
262  plt.clf()
263 
264 
265 if __name__ == '__main__':
266 
267  args = handle_arg_parsing()
268  config = read_config(args.config)
269 
270  start = time.time()
271  lattice_gen = LatticeGenerator(config)
272  minimal_set_trajectories = lattice_gen.run()
273  print(f'Finished Generating. Took {time.time() - start} seconds')
274 
275  write_to_json(args.output, minimal_set_trajectories, config)
276  save_visualizations(args.visualizations, minimal_set_trajectories)