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