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