Ruiguo1 commited on
Commit
a906776
·
verified ·
1 Parent(s): f4f8d06

Upload latest curated NAVSIM and Bench2Drive release with final checkpoints (part 4)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. navsim_v2/navsim/planning/simulation/planner/pdm_planner/proposal/pdm_generator.py +382 -0
  2. navsim_v2/navsim/planning/simulation/planner/pdm_planner/proposal/pdm_proposal.py +96 -0
  3. navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/__init__.py +0 -0
  4. navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_and_traffic_scorer.py +277 -0
  5. navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_comfort_metrics.py +467 -0
  6. navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_scorer.py +700 -0
  7. navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_scorer_utils.py +64 -0
  8. navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/scene_aggregator.py +124 -0
  9. navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/__init__.py +0 -0
  10. navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/batch_kinematic_bicycle.py +184 -0
  11. navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/batch_lqr.py +457 -0
  12. navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/batch_lqr_utils.py +238 -0
  13. navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/pdm_simulator.py +78 -0
  14. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/__init__.py +0 -0
  15. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/graph_search/__init__.py +0 -0
  16. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/graph_search/bfs_roadblock.py +146 -0
  17. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/graph_search/dijkstra.py +145 -0
  18. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_array_representation.py +282 -0
  19. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_emergency_brake.py +135 -0
  20. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_enums.py +181 -0
  21. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_geometry_utils.py +178 -0
  22. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_path.py +106 -0
  23. navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/route_utils.py +215 -0
  24. navsim_v2/navsim/planning/training/__init__.py +0 -0
  25. navsim_v2/navsim/planning/training/abstract_feature_target_builder.py +46 -0
  26. navsim_v2/navsim/planning/training/agent_lightning_module.py +126 -0
  27. navsim_v2/navsim/planning/training/callbacks/time_logging_callback.py +52 -0
  28. navsim_v2/navsim/planning/training/dataset.py +288 -0
  29. navsim_v2/navsim/planning/utils/multithreading/__init__.py +0 -0
  30. navsim_v2/navsim/planning/utils/multithreading/worker_ray_no_torch.py +159 -0
  31. navsim_v2/navsim/traffic_agents_policies/__init__.py +0 -0
  32. navsim_v2/navsim/traffic_agents_policies/abstract_traffic_agents_policy.py +177 -0
  33. navsim_v2/navsim/traffic_agents_policies/constant_velocity_traffic_agents.py +68 -0
  34. navsim_v2/navsim/traffic_agents_policies/log_replay_traffic_agents.py +60 -0
  35. navsim_v2/navsim/traffic_agents_policies/navsim_IDM_traffic_agents.py +139 -0
  36. navsim_v2/requirements.txt +41 -0
  37. navsim_v2/scripts/evaluation/eval_posttraj_temporal_v22.sh +39 -0
  38. navsim_v2/setup.py +30 -0
  39. scripts/eval_navsim_v1.sh +32 -0
  40. scripts/eval_navsim_v2.sh +15 -0
  41. scripts/prepare_bench2drive_eval.py +24 -0
  42. scripts/train_bench2drive.sh +4 -0
  43. scripts/train_navsim.sh +10 -0
  44. third_party/vjepa2/APACHE-LICENSE +201 -0
  45. third_party/vjepa2/LICENSE +21 -0
  46. third_party/vjepa2/evals/hub/__init__.py +0 -0
  47. third_party/vjepa2/evals/hub/preprocessor.py +15 -0
  48. third_party/vjepa2/hubconf.py +19 -0
  49. third_party/vjepa2/pyproject.toml +6 -0
  50. third_party/vjepa2/requirements.txt +25 -0
navsim_v2/navsim/planning/simulation/planner/pdm_planner/proposal/pdm_generator.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from typing import Dict, List, Optional
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ from nuplan.common.actor_state.agent import Agent
7
+ from nuplan.common.actor_state.car_footprint import CarFootprint
8
+ from nuplan.common.actor_state.ego_state import EgoState
9
+ from nuplan.common.actor_state.scene_object import SceneObject
10
+ from nuplan.common.actor_state.state_representation import StateSE2, TimePoint
11
+ from nuplan.common.actor_state.vehicle_parameters import VehicleParameters
12
+ from nuplan.common.geometry.transform import transform
13
+ from nuplan.planning.simulation.trajectory.interpolated_trajectory import InterpolatedTrajectory
14
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
15
+ from shapely.geometry import Point, Polygon
16
+ from shapely.geometry.base import CAP_STYLE
17
+
18
+ from navsim.planning.simulation.planner.pdm_planner.observation.pdm_observation import PDMObservation
19
+ from navsim.planning.simulation.planner.pdm_planner.proposal.pdm_proposal import PDMProposalManager
20
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_array_representation import state_array_to_ego_states
21
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import LeadingAgentIndex, StateIDMIndex, StateIndex
22
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_geometry_utils import normalize_angle
23
+
24
+
25
+ class PDMGenerator:
26
+ """Class to generate proposals in PDM."""
27
+
28
+ def __init__(
29
+ self,
30
+ trajectory_sampling: TrajectorySampling,
31
+ proposal_sampling: TrajectorySampling,
32
+ leading_agent_update_rate: int = 2,
33
+ ):
34
+ """
35
+ Constructor of PDMGenerator
36
+ :param trajectory_sampling: Sampling parameters for final trajectory
37
+ :param proposal_sampling: Sampling parameters for proposals
38
+ :param leading_agent_update_rate: sample update-rate of leading agent state, defaults to 2
39
+ """
40
+ assert (
41
+ trajectory_sampling.interval_length == proposal_sampling.interval_length
42
+ ), "PDMGenerator: Proposals and Trajectory must have equal interval length!"
43
+
44
+ # trajectory config
45
+ self._trajectory_sampling: int = trajectory_sampling
46
+ self._proposal_sampling: int = proposal_sampling
47
+ self._sample_interval: float = trajectory_sampling.interval_length
48
+
49
+ # generation config
50
+ self._leading_agent_update: int = leading_agent_update_rate
51
+
52
+ # lazy loaded
53
+ self._state_array: Optional[npt.NDArray[np.float64]] = None
54
+ self._state_idm_array: Optional[npt.NDArray[np.float64]] = None
55
+ self._leading_agent_array: Optional[npt.NDArray[np.float64]] = None
56
+
57
+ self._proposal_manager: Optional[PDMProposalManager] = None
58
+ self._observation: Optional[PDMObservation] = None
59
+
60
+ self._initial_ego_state: Optional[EgoState] = None
61
+ self._vehicle_parameters: Optional[VehicleParameters] = None
62
+
63
+ # caches
64
+ self._driving_corridor_cache: Optional[Dict[int, Polygon]] = None
65
+ self._time_point_list: Optional[List[TimePoint]] = None
66
+
67
+ def generate_proposals(
68
+ self,
69
+ initial_ego_state: EgoState,
70
+ observation: PDMObservation,
71
+ proposal_manager: PDMProposalManager,
72
+ ) -> npt.NDArray[np.float64]:
73
+ """
74
+ Generates proposals by unrolling IDM policies vor varying paths,
75
+ and saving the proposal states in array representation.
76
+ :param initial_ego_state: state of ego-vehicle at t=0
77
+ :param observation: PDMObservation class
78
+ :param proposal_manager: PDMProposalManager class
79
+ :return: unrolled proposal states in array representation
80
+ """
81
+ self._reset(initial_ego_state, observation, proposal_manager)
82
+ self._initialize_time_points()
83
+
84
+ # unroll proposals per path, to interpolate along batch-dim
85
+ lateral_batch_dict = self._get_lateral_batch_dict()
86
+
87
+ for lateral_idx, lateral_batch_idcs in lateral_batch_dict.items():
88
+ self._initialize_states(lateral_batch_idcs)
89
+ for time_idx in range(1, self._proposal_sampling.num_poses + 1, 1):
90
+ self._update_leading_agents(lateral_batch_idcs, time_idx)
91
+ self._update_idm_states(lateral_batch_idcs, time_idx)
92
+ self._update_states_se2(lateral_batch_idcs, time_idx)
93
+
94
+ return self._state_array
95
+
96
+ def generate_trajectory(
97
+ self,
98
+ proposal_idx: int,
99
+ ) -> InterpolatedTrajectory:
100
+ """
101
+ Complete unrolling of final trajectory to number of trajectory samples.
102
+ :param proposal_idx: index of best-scored proposal
103
+ :return: InterpolatedTrajectory class
104
+ """
105
+ assert (
106
+ len(self._time_point_list) == self._proposal_sampling.num_poses + 1
107
+ ), "PDMGenerator: Proposals must be generated first!"
108
+
109
+ lateral_batch_idcs = [proposal_idx]
110
+ current_time_point = copy.deepcopy(self._time_point_list[-1])
111
+
112
+ for time_idx in range(
113
+ self._proposal_sampling.num_poses + 1,
114
+ self._trajectory_sampling.num_poses + 1,
115
+ 1,
116
+ ):
117
+ current_time_point += TimePoint(int(self._sample_interval * 1e6))
118
+ self._time_point_list.append(current_time_point)
119
+
120
+ self._update_leading_agents(lateral_batch_idcs, time_idx)
121
+ self._update_idm_states(lateral_batch_idcs, time_idx)
122
+ self._update_states_se2(lateral_batch_idcs, time_idx)
123
+
124
+ # convert array representation to list of EgoState class
125
+ ego_states: List[EgoState] = state_array_to_ego_states(
126
+ self._state_array[proposal_idx],
127
+ self._time_point_list,
128
+ self._vehicle_parameters,
129
+ )
130
+ return InterpolatedTrajectory(ego_states)
131
+
132
+ def _reset(
133
+ self,
134
+ initial_ego_state: EgoState,
135
+ observation: PDMObservation,
136
+ proposal_manager: PDMProposalManager,
137
+ ) -> None:
138
+ """
139
+ Re-initializes several class attributes for unrolling in new iteration
140
+ :param initial_ego_state: ego-vehicle state at t=0
141
+ :param observation: PDMObservation class
142
+ :param proposal_manager: PDMProposalManager class
143
+ """
144
+
145
+ # lazy loading
146
+ self._proposal_manager: PDMProposalManager = proposal_manager
147
+ self._observation: PDMObservation = observation
148
+
149
+ self._initial_ego_state = initial_ego_state
150
+ self._vehicle_parameters = initial_ego_state.car_footprint.vehicle_parameters
151
+
152
+ # reset proposal state arrays
153
+ self._state_array: npt.NDArray[np.float64] = np.zeros(
154
+ (
155
+ len(self._proposal_manager),
156
+ self._trajectory_sampling.num_poses + 1,
157
+ StateIndex.size(),
158
+ ),
159
+ dtype=np.float64,
160
+ ) # x, y, heading
161
+ self._state_idm_array: npt.NDArray[np.float64] = np.zeros(
162
+ (len(self._proposal_manager), self._trajectory_sampling.num_poses + 1, 2),
163
+ dtype=np.float64,
164
+ ) # progress, velocity
165
+ self._leading_agent_array: npt.NDArray[np.float64] = np.zeros(
166
+ (len(self._proposal_manager), self._trajectory_sampling.num_poses + 1, 3),
167
+ dtype=np.float64,
168
+ ) # progress, velocity, rear-length
169
+
170
+ # reset caches
171
+ self._driving_corridor_cache: Dict[int, Polygon] = {}
172
+
173
+ self._time_point_list: List[TimePoint] = []
174
+ self._updated: bool = True
175
+
176
+ def _initialize_time_points(self) -> None:
177
+ """Initializes a list of TimePoint objects for proposal horizon."""
178
+ current_time_point = copy.deepcopy(self._initial_ego_state.time_point)
179
+ self._time_point_list = [current_time_point]
180
+ for time_idx in range(1, self._proposal_sampling.num_poses + 1, 1):
181
+ current_time_point += TimePoint(int(self._sample_interval * 1e6))
182
+ self._time_point_list.append(copy.deepcopy(current_time_point))
183
+
184
+ def _initialize_states(self, lateral_batch_idcs: List[int]) -> None:
185
+ """
186
+ Initializes all state arrays for ego, IDM, and leading agent at t=0
187
+ :param lateral_batch_idcs: list of proposal indices, sharing a path.
188
+ """
189
+
190
+ # all initial states are identical for shared lateral_idx
191
+ # thus states are created for lateral_batch_idcs[0] and repeated
192
+ dummy_proposal_idx = lateral_batch_idcs[0]
193
+
194
+ ego_position = Point(*self._initial_ego_state.rear_axle.point.array)
195
+
196
+ ego_progress = self._proposal_manager[dummy_proposal_idx].linestring.project(ego_position)
197
+ ego_velocity = self._initial_ego_state.dynamic_car_state.rear_axle_velocity_2d.x
198
+
199
+ self._state_idm_array[lateral_batch_idcs, 0, StateIDMIndex.PROGRESS] = ego_progress
200
+ self._state_idm_array[lateral_batch_idcs, 0, StateIDMIndex.VELOCITY] = ego_velocity
201
+
202
+ state_array = self._proposal_manager[dummy_proposal_idx].path.interpolate([ego_progress], as_array=True)[0]
203
+ self._state_array[lateral_batch_idcs, 0, StateIndex.STATE_SE2] = state_array
204
+
205
+ def _update_states_se2(self, lateral_batch_idcs: List[int], time_idx: int) -> None:
206
+ """
207
+ Updates state array for ego, at current time-step.
208
+ :param lateral_batch_idcs: list of proposal indices, sharing a path.
209
+ :param time_idx: index of unrolling iteration (for proposal/trajectory samples)
210
+ """
211
+ assert time_idx > 0, "PDMGenerator: call _initialize_states first!"
212
+ dummy_proposal_idx = lateral_batch_idcs[0]
213
+ current_progress = self._state_idm_array[lateral_batch_idcs, time_idx, StateIDMIndex.PROGRESS]
214
+ states_se2_array: npt.NDArray[np.float64] = self._proposal_manager[dummy_proposal_idx].path.interpolate(
215
+ current_progress, as_array=True
216
+ )
217
+ self._state_array[lateral_batch_idcs, time_idx, StateIndex.STATE_SE2] = states_se2_array
218
+
219
+ def _update_idm_states(self, lateral_batch_idcs: List[int], time_idx: int) -> None:
220
+ """
221
+ Updates idm state array, by propagating policy for one step.
222
+ :param lateral_batch_idcs: list of proposal indices, sharing a path.
223
+ :param time_idx: index of unrolling iteration (for proposal/trajectory samples)
224
+ """
225
+ assert time_idx > 0, "PDMGenerator: call _initialize_states first!"
226
+ longitudinal_idcs = [
227
+ self._proposal_manager[proposal_idx].longitudinal_idx for proposal_idx in lateral_batch_idcs
228
+ ]
229
+ next_idm_states = self._proposal_manager.longitudinal_policies.propagate(
230
+ self._state_idm_array[lateral_batch_idcs, time_idx - 1],
231
+ self._leading_agent_array[lateral_batch_idcs, time_idx],
232
+ longitudinal_idcs,
233
+ self._sample_interval,
234
+ )
235
+ self._state_idm_array[lateral_batch_idcs, time_idx] = next_idm_states
236
+
237
+ def _update_leading_agents(self, lateral_batch_idcs: List[int], time_idx: int) -> None:
238
+ """
239
+ Update leading agent state array by searching for agents/obstacles in driving corridor.
240
+ :param lateral_idx: index indicating the path of proposals
241
+ :param lateral_batch_idcs: list of proposal indices, sharing a path.
242
+ :param time_idx: index of unrolling iteration (for proposal/trajectory samples)
243
+ """
244
+ assert time_idx > 0, "PDMGenerator: call _initialize_states first!"
245
+
246
+ # update leading agent state at first call or at update rate (runtime)
247
+ update_leading_agent: bool = (time_idx % self._leading_agent_update) == 0
248
+
249
+ if not update_leading_agent:
250
+ self._leading_agent_array[lateral_batch_idcs, time_idx] = self._leading_agent_array[
251
+ lateral_batch_idcs, time_idx - 1
252
+ ]
253
+
254
+ else:
255
+ dummy_proposal_idx = lateral_batch_idcs[0]
256
+
257
+ leading_agent_array = np.zeros(len(LeadingAgentIndex), dtype=np.float64)
258
+ intersecting_objects: List[str] = self._get_intersecting_objects(lateral_batch_idcs, time_idx)
259
+
260
+ # collect all leading vehicles ones for all proposals (run-time)
261
+ object_progress_dict: Dict[str, float] = {}
262
+ for object in intersecting_objects:
263
+ if object not in self._observation.collided_track_ids:
264
+ object_progress = self._proposal_manager[dummy_proposal_idx].linestring.project(
265
+ self._observation[time_idx][object].centroid
266
+ )
267
+ object_progress_dict[object] = object_progress
268
+
269
+ # select leading agent for each proposal individually
270
+ for proposal_idx in lateral_batch_idcs:
271
+ current_ego_progress = self._state_idm_array[proposal_idx, time_idx - 1, StateIDMIndex.PROGRESS]
272
+
273
+ # filter all objects ahead
274
+ agents_ahead: Dict[str, float] = {
275
+ agent: progress
276
+ for agent, progress in object_progress_dict.items()
277
+ if progress > current_ego_progress
278
+ }
279
+
280
+ if len(agents_ahead) > 0: # red light, object or agent ahead
281
+ current_state_se2 = StateSE2(*self._state_array[proposal_idx, time_idx - 1, StateIndex.STATE_SE2])
282
+ ego_polygon: Polygon = CarFootprint.build_from_rear_axle(
283
+ current_state_se2, self._vehicle_parameters
284
+ ).oriented_box.geometry
285
+
286
+ relative_distances = [
287
+ ego_polygon.distance(self._observation[time_idx][agent]) for agent in agents_ahead.keys()
288
+ ]
289
+
290
+ argmin = np.argmin(relative_distances)
291
+ nearest_agent = list(agents_ahead.keys())[argmin]
292
+
293
+ # add rel. distance for red light, object or agent
294
+ relative_distance = current_ego_progress + relative_distances[argmin]
295
+ leading_agent_array[LeadingAgentIndex.PROGRESS] = relative_distance
296
+
297
+ # calculate projected velocity if not red light
298
+ if self._observation.red_light_token not in nearest_agent:
299
+ leading_agent_array[LeadingAgentIndex.VELOCITY] = self._get_leading_agent_velocity(
300
+ current_state_se2.heading,
301
+ self._observation.unique_objects[nearest_agent],
302
+ )
303
+
304
+ else: # nothing ahead, free driving
305
+ path_length = self._proposal_manager[proposal_idx].linestring.length
306
+ path_rear = self._vehicle_parameters.length / 2
307
+
308
+ leading_agent_array[LeadingAgentIndex.PROGRESS] = path_length
309
+ leading_agent_array[LeadingAgentIndex.LENGTH_REAR] = path_rear
310
+
311
+ self._leading_agent_array[proposal_idx, time_idx] = leading_agent_array
312
+
313
+ @staticmethod
314
+ def _get_leading_agent_velocity(ego_heading: float, agent: SceneObject) -> float:
315
+ """
316
+ Calculates velocity of leading vehicle projected to ego's heading.
317
+ :param ego_heading: heading angle [rad]
318
+ :param agent: SceneObject class
319
+ :return: projected velocity [m/s]
320
+ """
321
+
322
+ if isinstance(agent, Agent): # dynamic object
323
+ relative_heading = normalize_angle(agent.center.heading - ego_heading)
324
+ projected_velocity = transform(
325
+ StateSE2(agent.velocity.magnitude(), 0, 0),
326
+ StateSE2(0, 0, relative_heading).as_matrix(),
327
+ ).x
328
+ else: # static object
329
+ projected_velocity = 0.0
330
+
331
+ return projected_velocity
332
+
333
+ def _get_intersecting_objects(self, lateral_batch_idcs: List[int], time_idx: int) -> List[str]:
334
+ """
335
+ Returns and caches all intersecting objects for the proposals path and time-step.
336
+ :param lateral_batch_idcs: list of proposal indices, sharing a path
337
+ :param time_idx: index indicating the path of proposals
338
+ :return: list of object tokens
339
+ """
340
+ dummy_proposal_idx = lateral_batch_idcs[0]
341
+ driving_corridor: Polygon = self._get_driving_corridor(dummy_proposal_idx)
342
+ return self._observation[time_idx].intersects(driving_corridor)
343
+
344
+ def _get_driving_corridor(self, proposal_idx: int) -> Polygon:
345
+ """
346
+ Creates and caches driving corridor of ego-vehicle for each proposal path.
347
+ :param proposal_idx: index of a proposal
348
+ :return: linestring of max trajectory distance and ego's width
349
+ """
350
+ lateral_idx = self._proposal_manager[proposal_idx].lateral_idx
351
+
352
+ if lateral_idx not in self._driving_corridor_cache.keys():
353
+ ego_distance = self._state_idm_array[proposal_idx, 0, StateIDMIndex.PROGRESS]
354
+ trajectory_distance = (
355
+ ego_distance
356
+ + abs(self._proposal_manager.max_target_velocity)
357
+ * self._trajectory_sampling.num_poses
358
+ * self._sample_interval
359
+ )
360
+ linestring_ahead = self._proposal_manager[proposal_idx].path.substring(ego_distance, trajectory_distance)
361
+ expanded_path = linestring_ahead.buffer(self._vehicle_parameters.width / 2, cap_style=CAP_STYLE.square)
362
+
363
+ self._driving_corridor_cache[lateral_idx] = expanded_path
364
+
365
+ return self._driving_corridor_cache[lateral_idx]
366
+
367
+ def _get_lateral_batch_dict(self) -> Dict[int, List[int]]:
368
+ """
369
+ Creates a dictionary for lateral paths and their proposal indices.
370
+ :return: dictionary of lateral and proposal indices
371
+ """
372
+ lateral_batch_dict: Dict[int, List[int]] = {}
373
+
374
+ for proposal_idx in range(len(self._proposal_manager)):
375
+ lateral_idx = self._proposal_manager[proposal_idx].lateral_idx
376
+
377
+ if lateral_idx not in lateral_batch_dict.keys():
378
+ lateral_batch_dict[lateral_idx] = [proposal_idx]
379
+ else:
380
+ lateral_batch_dict[lateral_idx].append(proposal_idx)
381
+
382
+ return lateral_batch_dict
navsim_v2/navsim/planning/simulation/planner/pdm_planner/proposal/pdm_proposal.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+
4
+ from shapely.geometry import LineString
5
+
6
+ from navsim.planning.simulation.planner.pdm_planner.proposal.batch_idm_policy import BatchIDMPolicy
7
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_path import PDMPath
8
+
9
+
10
+ @dataclass
11
+ class PDMProposal:
12
+ """Dataclass for storing proposal information."""
13
+
14
+ proposal_idx: int
15
+ lateral_idx: int
16
+ longitudinal_idx: int
17
+ path: PDMPath
18
+
19
+ @property
20
+ def linestring(self) -> LineString:
21
+ """Getter for linestring of proposal's path."""
22
+ return self.path.linestring
23
+
24
+ @property
25
+ def length(self):
26
+ """Getter for length [m] of proposal's path."""
27
+ return self.path.length
28
+
29
+
30
+ class PDMProposalManager:
31
+ """Class to store and manage lateral and longitudinal combination of proposals."""
32
+
33
+ def __init__(
34
+ self,
35
+ lateral_proposals: List[PDMPath],
36
+ longitudinal_policies: BatchIDMPolicy,
37
+ ):
38
+ """
39
+ Constructor for PDMProposalManager
40
+ :param lateral_proposals: list of path's to follow
41
+ :param longitudinal_policies: IDM policy class (batch-wise)
42
+ """
43
+
44
+ self._num_lateral_proposals: int = len(lateral_proposals)
45
+ self._num_longitudinal_proposals: int = longitudinal_policies.num_policies
46
+ self._longitudinal_policies: BatchIDMPolicy = longitudinal_policies
47
+
48
+ self._proposals: List[PDMProposal] = []
49
+ proposal_idx = 0
50
+
51
+ for lateral_idx in range(self._num_lateral_proposals):
52
+ for longitudinal_idx in range(self._num_longitudinal_proposals):
53
+ self._proposals.append(
54
+ PDMProposal(
55
+ proposal_idx=proposal_idx,
56
+ lateral_idx=lateral_idx,
57
+ longitudinal_idx=longitudinal_idx,
58
+ path=lateral_proposals[lateral_idx],
59
+ )
60
+ )
61
+ proposal_idx += 1
62
+
63
+ def __len__(self) -> int:
64
+ """Returns number of proposals (paths x policies)."""
65
+ return len(self._proposals)
66
+
67
+ def __getitem__(self, proposal_idx) -> PDMProposal:
68
+ """
69
+ Returns the requested proposal.
70
+ :param proposal_idx: index for each proposal
71
+ :return: PDMProposal dataclass
72
+ """
73
+ return self._proposals[proposal_idx]
74
+
75
+ def update(self, speed_limit_mps: float) -> None:
76
+ """
77
+ Updates target velocities of IDM policies with current speed-limit.
78
+ :param speed_limit_mps: current speed-limit [m/s]
79
+ """
80
+ self._longitudinal_policies.update(speed_limit_mps)
81
+
82
+ @property
83
+ def num_lateral_proposals(self) -> int:
84
+ return self._num_lateral_proposals
85
+
86
+ @property
87
+ def num_longitudinal_proposals(self) -> int:
88
+ return self._longitudinal_policies._num_longitudinal_proposals
89
+
90
+ @property
91
+ def max_target_velocity(self) -> float:
92
+ return self._longitudinal_policies.max_target_velocity
93
+
94
+ @property
95
+ def longitudinal_policies(self) -> BatchIDMPolicy:
96
+ return self._longitudinal_policies
navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/__init__.py ADDED
File without changes
navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_and_traffic_scorer.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+ from typing import List, Tuple
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ import pandas as pd
7
+ from nuplan.common.actor_state.agent import Agent
8
+ from nuplan.common.actor_state.oriented_box import OrientedBox
9
+ from nuplan.common.actor_state.scene_object import SceneObjectMetadata
10
+ from nuplan.common.actor_state.state_representation import StateSE2, StateVector2D
11
+ from nuplan.common.actor_state.tracked_objects import TrackedObjects
12
+ from nuplan.common.actor_state.tracked_objects_types import TrackedObjectType
13
+ from nuplan.common.actor_state.vehicle_parameters import VehicleParameters, get_pacifica_parameters
14
+ from nuplan.common.maps.maps_datatypes import SemanticMapLayer
15
+ from nuplan.common.maps.nuplan_map.map_factory import get_maps_api
16
+ from nuplan.common.maps.nuplan_map.nuplan_map import NuPlanMap
17
+ from nuplan.planning.simulation.observation.observation_type import DetectionsTracks
18
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
19
+ from shapely import Point
20
+
21
+ from navsim.common.dataclasses import PDMResults
22
+ from navsim.planning.metric_caching.metric_cache import MapParameters
23
+ from navsim.planning.simulation.planner.pdm_planner.observation.pdm_observation import PDMObservation
24
+ from navsim.planning.simulation.planner.pdm_planner.observation.pdm_occupancy_map import PDMDrivableMap
25
+ from navsim.planning.simulation.planner.pdm_planner.scoring.pdm_scorer import PDMScorer, PDMScorerConfig
26
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import StateIndex
27
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_path import PDMPath
28
+ from navsim.traffic_agents_policies.abstract_traffic_agents_policy import (
29
+ extract_vehicle_trajectories_from_detections_tracks,
30
+ )
31
+
32
+
33
+ class PaddingTrackedObject:
34
+ def __init__(self, track_token):
35
+ self.track_token = track_token
36
+ self.box = OrientedBox(
37
+ center=StateSE2(x=np.inf, y=np.inf, heading=0),
38
+ length=0,
39
+ width=0,
40
+ height=0,
41
+ )
42
+ self.velocity = StateVector2D(x=0, y=0)
43
+ self.tracked_object_type = TrackedObjectType.VEHICLE
44
+ self.center = self.box.center
45
+
46
+
47
+ class PDMTrafficScorer(PDMScorer):
48
+ def __init__(
49
+ self,
50
+ proposal_sampling: TrajectorySampling,
51
+ config: PDMScorerConfig = PDMScorerConfig(),
52
+ vehicle_parameters: VehicleParameters = get_pacifica_parameters(),
53
+ ):
54
+ super().__init__(proposal_sampling, config, vehicle_parameters)
55
+
56
+ def extract_centerline_for_agent(
57
+ self, agent_states: List[npt.NDArray[np.float64]], map_api: NuPlanMap
58
+ ) -> Tuple[PDMPath, List[int]]:
59
+ # find starting lane for agent
60
+ proximal_objects = map_api.get_proximal_map_objects(
61
+ point=Point(agent_states[0][StateIndex.X], agent_states[0][StateIndex.Y]),
62
+ layers=[SemanticMapLayer.LANE, SemanticMapLayer.LANE_CONNECTOR],
63
+ radius=10.0,
64
+ )
65
+ nearby_lanes = [map_obj for map_objects in proximal_objects.values() for map_obj in map_objects]
66
+ if len(nearby_lanes) == 0:
67
+ return None, None
68
+ elif len(nearby_lanes) == 1:
69
+ starting_lane = nearby_lanes[0]
70
+ else:
71
+ agent_point = Point(agent_states[0][StateIndex.X], agent_states[0][StateIndex.Y])
72
+ dist_to_lanes = [lane.polygon.distance(agent_point) for lane in nearby_lanes]
73
+ starting_lane = nearby_lanes[np.argmin(dist_to_lanes)]
74
+
75
+ # match trajectory onto successor lanes
76
+ agent_lanes = [starting_lane]
77
+ for agent_state in agent_states[1:]:
78
+ # try to match state to one of the outgoing lanes
79
+ outgoing_lanes = starting_lane.outgoing_edges
80
+ candidate_lanes = outgoing_lanes + agent_lanes
81
+ agent_point = Point(agent_state[StateIndex.X], agent_state[StateIndex.Y])
82
+ dist_to_lanes = [lane.polygon.distance(agent_point) for lane in candidate_lanes]
83
+ closest_lane = candidate_lanes[np.argmin(dist_to_lanes)]
84
+ agent_lanes.append(closest_lane)
85
+
86
+ # return PDM-Path and lane ids
87
+ agent_centerline = PDMPath(
88
+ discrete_path=[state for lane in agent_lanes for state in lane.baseline_path.discrete_path],
89
+ )
90
+ lane_ids = [lane.id for lane in agent_lanes]
91
+
92
+ return agent_centerline, lane_ids
93
+
94
+ def build_ego_tracked_object_states(
95
+ self,
96
+ states: npt.NDArray[np.float64],
97
+ ) -> List[Agent]:
98
+ vehicle_parameters = get_pacifica_parameters()
99
+ return [
100
+ Agent(
101
+ tracked_object_type=TrackedObjectType.VEHICLE,
102
+ oriented_box=OrientedBox(
103
+ center=StateSE2(x=state[StateIndex.X], y=state[StateIndex.Y], heading=state[StateIndex.HEADING]),
104
+ length=vehicle_parameters.length,
105
+ width=vehicle_parameters.width,
106
+ height=vehicle_parameters.height,
107
+ ),
108
+ velocity=StateVector2D(x=state[StateIndex.VELOCITY_X], y=state[StateIndex.VELOCITY_Y]),
109
+ metadata=SceneObjectMetadata(
110
+ timestamp_us=0,
111
+ token=None,
112
+ track_id=None,
113
+ track_token="ego",
114
+ ),
115
+ angular_velocity=state[StateIndex.ANGULAR_VELOCITY],
116
+ )
117
+ for state in states
118
+ ]
119
+
120
+ def build_agent_centric_observation(
121
+ self,
122
+ observation: PDMObservation,
123
+ traffic_agent_detections_tracks: List[DetectionsTracks],
124
+ ego_tracked_objects: List[Agent],
125
+ agent_token: str,
126
+ ) -> PDMObservation:
127
+
128
+ agent_centric_observation = deepcopy(observation)
129
+
130
+ # remove target agent tracks from observation
131
+ # and add ego agent to it
132
+ agent_centric_detection_tracks = [
133
+ DetectionsTracks(
134
+ tracked_objects=TrackedObjects(
135
+ [o for o in tracks.tracked_objects if o.track_token != agent_token] + [ego_tracked_objects[t]]
136
+ )
137
+ )
138
+ for t, tracks in enumerate(traffic_agent_detections_tracks)
139
+ ]
140
+
141
+ agent_centric_observation.update_detections_tracks(
142
+ detection_tracks=agent_centric_detection_tracks,
143
+ )
144
+ return agent_centric_observation
145
+
146
+ def build_ego_centric_observation(
147
+ self,
148
+ observation: PDMObservation,
149
+ traffic_agent_detections_tracks: List[DetectionsTracks],
150
+ ) -> PDMObservation:
151
+ ego_centric_observation = deepcopy(observation)
152
+ ego_centric_observation.update_detections_tracks(
153
+ detection_tracks=traffic_agent_detections_tracks,
154
+ )
155
+ return ego_centric_observation
156
+
157
+ def score_proposals(
158
+ self,
159
+ states: npt.NDArray[np.float64],
160
+ observation: PDMObservation,
161
+ centerline: PDMPath,
162
+ route_lane_ids: List[str],
163
+ drivable_area_map: PDMDrivableMap,
164
+ map_parameters: MapParameters,
165
+ simulated_agent_detections_tracks: List[DetectionsTracks],
166
+ ) -> List[pd.DataFrame]:
167
+
168
+ map_api = get_maps_api(map_parameters.map_root, map_parameters.map_version, map_parameters.map_name)
169
+
170
+ # Observations need to be one second longer than the ego-trajectory to calculate ego ttc metrics
171
+ # Thus, we slice the traffic agents trajectories to only evaluate the first four seconds
172
+ trajectory_length = states.shape[1]
173
+ (
174
+ logreplay_agent_trajectories,
175
+ logreplay_agent_masks,
176
+ logreplay_agent_tokens,
177
+ ) = extract_vehicle_trajectories_from_detections_tracks(
178
+ detections_tracks=observation.detections_tracks[:trajectory_length],
179
+ reverse_padding=False,
180
+ )
181
+ (
182
+ simulated_agent_trajectories,
183
+ simulated_agent_masks,
184
+ simulated_agent_tokens,
185
+ ) = extract_vehicle_trajectories_from_detections_tracks(
186
+ detections_tracks=simulated_agent_detections_tracks[:trajectory_length],
187
+ reverse_padding=False,
188
+ )
189
+ # The log-replay agents might contain more agents than the simulated agents
190
+ # since the log-replay also covers agents not visible at the current state, which are
191
+ # interpolated to the current timestamp. The simulated agents only contain the agents
192
+ # that are visible at the current state. We need to make sure to access the correct
193
+ # log-replay trajectory for each agent, so we store them in a dictionary with the agent token as key
194
+ simulated_agents = {
195
+ token: (simulated_agent_trajectory, simulated_agent_mask)
196
+ for (token, simulated_agent_trajectory, simulated_agent_mask) in zip(
197
+ simulated_agent_tokens, simulated_agent_trajectories, simulated_agent_masks
198
+ )
199
+ }
200
+ logreplay_agents = {
201
+ token: (logreplay_agent_trajectory, logreplay_agent_mask)
202
+ for (token, logreplay_agent_trajectory, logreplay_agent_mask) in zip(
203
+ logreplay_agent_tokens, logreplay_agent_trajectories, logreplay_agent_masks
204
+ )
205
+ }
206
+
207
+ # We evaluate traffic agents with respect to the ego proposal at index 1
208
+ # which refers to the model outut. Note: This is very hacky and should be refactored
209
+ pred_idx = 1
210
+ ego_tracked_objects = self.build_ego_tracked_object_states(states[pred_idx])
211
+
212
+ agent_scores = []
213
+ for agent_token in simulated_agent_tokens:
214
+ agent_mask = simulated_agents[agent_token][1]
215
+ agent_trajectory = simulated_agents[agent_token][0]
216
+ logreplay_agent_trajectory = logreplay_agents[agent_token][0]
217
+ if not np.all(agent_mask):
218
+ # agent is not observed the whole time, so we skip it
219
+ continue
220
+
221
+ # extract centerline for agent
222
+ agent_centerline, lane_ids = self.extract_centerline_for_agent(
223
+ agent_states=agent_trajectory, map_api=map_api
224
+ )
225
+ if agent_centerline is None:
226
+ # agent is too far away from the map, so we skip it
227
+ continue
228
+
229
+ # for each agent, we need to remove its own tracks from the observation and add ego
230
+ agent_centric_observation = self.build_agent_centric_observation(
231
+ observation=observation,
232
+ ego_tracked_objects=ego_tracked_objects,
233
+ traffic_agent_detections_tracks=simulated_agent_detections_tracks,
234
+ agent_token=agent_token,
235
+ )
236
+
237
+ # we also evaluate the log-replay future for each agent, otherwise progress metric is meaningless
238
+ agent_proposals = np.stack([agent_trajectory, logreplay_agent_trajectory], axis=0)
239
+
240
+ # evaluate the trajectory of the agent
241
+ agent_score = super().score_proposals(
242
+ states=agent_proposals,
243
+ observation=agent_centric_observation,
244
+ centerline=agent_centerline,
245
+ route_lane_ids=lane_ids,
246
+ drivable_area_map=drivable_area_map,
247
+ )[0]
248
+ agent_scores.append(agent_score)
249
+
250
+ if len(agent_scores) > 0:
251
+ agent_scores = pd.concat(agent_scores)
252
+ agent_scores = agent_scores.mean(axis=0).to_frame().T.add_prefix("traffic_")
253
+ else:
254
+ agent_scores = pd.DataFrame([PDMResults.get_empty_results()])
255
+ agent_scores = agent_scores.add_prefix("traffic_")
256
+
257
+ # make sure to use the reactive agents here
258
+ ego_centric_observation = self.build_ego_centric_observation(
259
+ observation=observation,
260
+ traffic_agent_detections_tracks=simulated_agent_detections_tracks,
261
+ )
262
+
263
+ ego_scores = super().score_proposals(
264
+ states=states,
265
+ observation=ego_centric_observation,
266
+ centerline=centerline,
267
+ route_lane_ids=route_lane_ids,
268
+ drivable_area_map=drivable_area_map,
269
+ )
270
+
271
+ # we have exactly one future traffic scenario in the metric cache,
272
+ # but potentially multiple ego-proposals
273
+ # (usually at least two, i.e. one from PDM and one from the Agent).
274
+ # Thus, we append the traffic scores to each ego score result.
275
+ results = [pd.concat([ego_score, agent_scores], axis=1) for ego_score in ego_scores]
276
+
277
+ return results
navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_comfort_metrics.py ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ from nuplan.common.actor_state.vehicle_parameters import VehicleParameters, get_pacifica_parameters
6
+ from scipy.signal import savgol_filter
7
+
8
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_array_representation import (
9
+ state_array_to_center_state_array,
10
+ )
11
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import StateIndex
12
+
13
+ # TODO: Refactor & add to config
14
+
15
+ # (1) ego_jerk_metric,
16
+ MAX_ABS_MAG_JERK: float = 8.37 # [m/s^3]
17
+
18
+ # (2) ego_lat_acceleration_metric
19
+ MAX_ABS_LAT_ACCEL: float = 4.89 # [m/s^2]
20
+
21
+ # (3) ego_lon_acceleration_metric
22
+ MAX_LON_ACCEL: float = 2.40 # [m/s^2]
23
+ MIN_LON_ACCEL: float = -4.05
24
+
25
+ # (4) ego_yaw_acceleration_metric
26
+ MAX_ABS_YAW_ACCEL: float = 1.93 # [rad/s^2]
27
+
28
+ # (5) ego_lon_jerk_metric
29
+ MAX_ABS_LON_JERK: float = 4.13 # [m/s^3]
30
+
31
+ # (6) ego_yaw_rate_metric
32
+ MAX_ABS_YAW_RATE: float = 0.95 # [rad/s]
33
+
34
+
35
+ # Extended Comfort thresholds
36
+ acceleration_threshold: float = 0.7 # [m/s^2]
37
+ jerk_threshold: float = 0.5 # [m/s^3]
38
+ yaw_rate_threshold: float = 0.1 # [rad/s]
39
+ yaw_accel_threshold: float = 0.1 # [rad/s^2]
40
+
41
+
42
+ def _extract_ego_acceleration(
43
+ states: npt.NDArray[np.float64],
44
+ acceleration_coordinate: str,
45
+ vehicle_parameters: VehicleParameters,
46
+ decimals: int = 8,
47
+ poly_order: int = 2,
48
+ window_length: int = 8,
49
+ ) -> npt.NDArray[np.float32]:
50
+ """
51
+ Extract acceleration of ego pose in simulation history over batch-dim
52
+ :param states: array representation of ego state values
53
+ :param acceleration_coordinate: string of axis to extract
54
+ :param vehicle_parameters: parameters of vehicle
55
+ :param decimals: decimal precision, defaults to 8
56
+ :param poly_order: polynomial order, defaults to 2
57
+ :param window_length: window size for extraction, defaults to 8
58
+ :raises ValueError: when coordinate not available
59
+ :return: array containing acceleration values
60
+ """
61
+
62
+ n_batch, n_time, n_states = states.shape
63
+ if acceleration_coordinate in ["x", "y"]:
64
+ center_states = state_array_to_center_state_array(states, vehicle_parameters)
65
+ coordinate_index = StateIndex.ACCELERATION_X if acceleration_coordinate == "x" else StateIndex.ACCELERATION_Y
66
+ acceleration: npt.NDArray[np.float64] = center_states[..., coordinate_index]
67
+
68
+ elif acceleration_coordinate == "magnitude":
69
+ acceleration: npt.NDArray[np.float64] = np.hypot(
70
+ states[..., StateIndex.ACCELERATION_X],
71
+ states[..., StateIndex.ACCELERATION_Y],
72
+ )
73
+ else:
74
+ raise ValueError(
75
+ f"acceleration_coordinate option: {acceleration_coordinate} not available. "
76
+ f"Available options are: x, y or magnitude"
77
+ )
78
+
79
+ acceleration = savgol_filter(
80
+ acceleration,
81
+ polyorder=poly_order,
82
+ window_length=min(window_length, n_time),
83
+ axis=-1,
84
+ )
85
+ acceleration = np.round(acceleration, decimals=decimals)
86
+ return acceleration
87
+
88
+
89
+ def _extract_ego_jerk(
90
+ states: npt.NDArray[np.float64],
91
+ acceleration_coordinate: str,
92
+ time_steps_s: npt.NDArray[np.float64],
93
+ vehicle_parameters: VehicleParameters,
94
+ decimals: int = 8,
95
+ deriv_order: int = 1,
96
+ poly_order: int = 2,
97
+ window_length: int = 15,
98
+ ) -> npt.NDArray[np.float32]:
99
+ """
100
+ Extract jerk of ego pose in simulation history over batch-dim
101
+ :param states: array representation of ego state values
102
+ :param acceleration_coordinate: string of axis to extract
103
+ :param time_steps_s: time steps [s] of time dim
104
+ :param vehicle_parameters: parameters of vehicle
105
+ :param decimals: decimal precision, defaults to 8
106
+ :param deriv_order: order of derivative, defaults to 1
107
+ :param poly_order: polynomial order, defaults to 2
108
+ :param window_length: window size for extraction, defaults to 15
109
+ :return: array containing jerk values
110
+ """
111
+ n_batch, n_time, n_states = states.shape
112
+ ego_acceleration = _extract_ego_acceleration(
113
+ states,
114
+ acceleration_coordinate=acceleration_coordinate,
115
+ vehicle_parameters=vehicle_parameters,
116
+ )
117
+ jerk = _approximate_derivatives(
118
+ ego_acceleration,
119
+ time_steps_s,
120
+ deriv_order=deriv_order,
121
+ poly_order=poly_order,
122
+ window_length=min(window_length, n_time),
123
+ )
124
+ jerk = np.round(jerk, decimals=decimals)
125
+ return jerk
126
+
127
+
128
+ def _extract_ego_yaw_rate(
129
+ states: npt.NDArray[np.float64],
130
+ time_steps_s: npt.NDArray[np.float64],
131
+ deriv_order: int = 1,
132
+ poly_order: int = 2,
133
+ decimals: int = 8,
134
+ window_length: int = 15,
135
+ ) -> npt.NDArray[np.float32]:
136
+ """
137
+ Extract yaw-rate of simulation history over batch-dim
138
+ :param states: array representation of ego state values
139
+ :param time_steps_s: time steps [s] of time dim
140
+ :param deriv_order: order of derivative, defaults to 1
141
+ :param poly_order: polynomial order, defaults to 2
142
+ :param decimals: decimal precision, defaults to 8
143
+ :param window_length: window size for extraction, defaults to 15
144
+ :return: array containing ego's yaw rate
145
+ """
146
+ ego_headings = states[..., StateIndex.HEADING]
147
+ ego_yaw_rate = _approximate_derivatives(
148
+ _phase_unwrap(ego_headings),
149
+ time_steps_s,
150
+ deriv_order=deriv_order,
151
+ poly_order=poly_order,
152
+ ) # convert to seconds
153
+ ego_yaw_rate = np.round(ego_yaw_rate, decimals=decimals)
154
+ return ego_yaw_rate
155
+
156
+
157
+ def _phase_unwrap(headings: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
158
+ """
159
+ Returns an array of heading angles equal mod 2 pi to the input heading angles,
160
+ and such that the difference between successive output angles is less than or
161
+ equal to pi radians in absolute value
162
+ :param headings: An array of headings (radians)
163
+ :return The phase-unwrapped equivalent headings.
164
+ """
165
+ # There are some jumps in the heading (e.g. from -np.pi to +np.pi) which causes approximation of yaw to be very large.
166
+ # We want unwrapped[j] = headings[j] - 2*pi*adjustments[j] for some integer-valued adjustments making the absolute value of
167
+ # unwrapped[j+1] - unwrapped[j] at most pi:
168
+ # -pi <= headings[j+1] - headings[j] - 2*pi*(adjustments[j+1] - adjustments[j]) <= pi
169
+ # -1/2 <= (headings[j+1] - headings[j])/(2*pi) - (adjustments[j+1] - adjustments[j]) <= 1/2
170
+ # So adjustments[j+1] - adjustments[j] = round((headings[j+1] - headings[j]) / (2*pi)).
171
+ two_pi = 2.0 * np.pi
172
+ adjustments = np.zeros_like(headings)
173
+ adjustments[..., 1:] = np.cumsum(np.round(np.diff(headings, axis=-1) / two_pi), axis=-1)
174
+ unwrapped = headings - two_pi * adjustments
175
+ return unwrapped
176
+
177
+
178
+ def _approximate_derivatives(
179
+ y: npt.NDArray[np.float32],
180
+ x: npt.NDArray[np.float32],
181
+ window_length: int = 5,
182
+ poly_order: int = 2,
183
+ deriv_order: int = 1,
184
+ axis: int = -1,
185
+ ) -> npt.NDArray[np.float32]:
186
+ """
187
+ Given two equal-length sequences y and x, compute an approximation to the n-th
188
+ derivative of some function interpolating the (x, y) data points, and return its
189
+ values at the x's. We assume the x's are increasing and equally-spaced.
190
+ :param y: The dependent variable (say of length n)
191
+ :param x: The independent variable (must have the same length n). Must be strictly
192
+ increasing and equally-spaced.
193
+ :param window_length: The order (default 5) of the Savitsky-Golay filter used.
194
+ (Ignored if the x's are not equally-spaced.) Must be odd and at least 3
195
+ :param poly_order: The degree (default 2) of the filter polynomial used. Must
196
+ be less than the window_length
197
+ :param deriv_order: The order of derivative to compute (default 1)
198
+ :param axis: The axis of the array x along which the filter is to be applied. Default is -1.
199
+ :return Derivatives.
200
+ """
201
+ window_length = min(window_length, len(x))
202
+
203
+ if not (poly_order < window_length):
204
+ raise ValueError(f"{poly_order} < {window_length} does not hold!")
205
+
206
+ dx = np.diff(x, axis=-1)
207
+ if not (dx > 0).all():
208
+ raise RuntimeError("dx is not monotonically increasing!")
209
+
210
+ dx = dx.mean()
211
+ derivative: npt.NDArray[np.float32] = savgol_filter(
212
+ y,
213
+ polyorder=poly_order,
214
+ window_length=window_length,
215
+ deriv=deriv_order,
216
+ delta=dx,
217
+ axis=axis,
218
+ )
219
+ return derivative
220
+
221
+
222
+ def _within_bound(
223
+ metric: npt.NDArray[np.float64],
224
+ min_bound: Optional[float] = None,
225
+ max_bound: Optional[float] = None,
226
+ ) -> npt.NDArray[np.bool_]:
227
+ """
228
+ Determines wether values in batch-dim are within bounds.
229
+ :param metric: metric values
230
+ :param min_bound: minimum bound, defaults to None
231
+ :param max_bound: maximum bound, defaults to None
232
+ :return: array of booleans wether metric values are within bounds
233
+ """
234
+ min_bound = min_bound if min_bound else float(-np.inf)
235
+ max_bound = max_bound if max_bound else float(np.inf)
236
+ metric_values = np.array(metric)
237
+ metric_within_bound = (metric_values > min_bound) & (metric_values < max_bound)
238
+ return np.all(metric_within_bound, axis=-1)
239
+
240
+
241
+ def _compute_lon_acceleration(
242
+ states: npt.NDArray[np.float64],
243
+ time_steps_s: npt.NDArray[np.float64],
244
+ vehicle_parameters: VehicleParameters,
245
+ ) -> npt.NDArray[np.bool_]:
246
+ """
247
+ Compute longitudinal acceleration over batch-dim of simulated proposals
248
+ :param states: array representation of ego state values
249
+ :param time_steps_s: time steps [s] of time dim
250
+ :param vehicle_parameters: parameters of vehicle
251
+ :return: longitudinal acceleration within bound
252
+ """
253
+ lon_acceleration = _extract_ego_acceleration(
254
+ states, acceleration_coordinate="x", vehicle_parameters=vehicle_parameters
255
+ )
256
+ return _within_bound(lon_acceleration, min_bound=MIN_LON_ACCEL, max_bound=MAX_LON_ACCEL)
257
+
258
+
259
+ def _compute_lat_acceleration(
260
+ states: npt.NDArray[np.float64],
261
+ time_steps_s: npt.NDArray[np.float64],
262
+ vehicle_parameters: VehicleParameters,
263
+ ) -> npt.NDArray[np.bool_]:
264
+ """
265
+ Compute lateral acceleration over batch-dim of simulated proposals
266
+ :param states: array representation of ego state values
267
+ :param time_steps_s: time steps [s] of time dim
268
+ :param vehicle_parameters: parameters of vehicle
269
+ :return: lateral acceleration within bound
270
+ """
271
+ lat_acceleration = _extract_ego_acceleration(
272
+ states, acceleration_coordinate="y", vehicle_parameters=vehicle_parameters
273
+ )
274
+ return _within_bound(lat_acceleration, min_bound=-MAX_ABS_LAT_ACCEL, max_bound=MAX_ABS_LAT_ACCEL)
275
+
276
+
277
+ def _compute_jerk_metric(
278
+ states: npt.NDArray[np.float64],
279
+ time_steps_s: npt.NDArray[np.float64],
280
+ vehicle_parameters: VehicleParameters,
281
+ ) -> npt.NDArray[np.bool_]:
282
+ """
283
+ Compute absolute jerk over batch-dim of simulated proposals
284
+ :param states: array representation of ego state values
285
+ :param time_steps_s: time steps [s] of time dim
286
+ :param vehicle_parameters: parameters of vehicle
287
+ :return: absolute jerk within bound
288
+ """
289
+ jerk_metric = _extract_ego_jerk(
290
+ states,
291
+ acceleration_coordinate="magnitude",
292
+ time_steps_s=time_steps_s,
293
+ vehicle_parameters=vehicle_parameters,
294
+ )
295
+ return _within_bound(jerk_metric, min_bound=-MAX_ABS_MAG_JERK, max_bound=MAX_ABS_MAG_JERK)
296
+
297
+
298
+ def _compute_lon_jerk_metric(
299
+ states: npt.NDArray[np.float64],
300
+ time_steps_s: npt.NDArray[np.float64],
301
+ vehicle_parameters: VehicleParameters,
302
+ ) -> npt.NDArray[np.bool_]:
303
+ """
304
+ Compute longitudinal jerk over batch-dim of simulated proposals
305
+ :param states: array representation of ego state values
306
+ :param time_steps_s: time steps [s] of time dim
307
+ :param vehicle_parameters: parameters of vehicle
308
+ :return: longitudinal jerk within bound
309
+ """
310
+ lon_jerk_metric = _extract_ego_jerk(
311
+ states,
312
+ acceleration_coordinate="x",
313
+ time_steps_s=time_steps_s,
314
+ vehicle_parameters=vehicle_parameters,
315
+ )
316
+ return _within_bound(lon_jerk_metric, min_bound=-MAX_ABS_LON_JERK, max_bound=MAX_ABS_LON_JERK)
317
+
318
+
319
+ def _compute_yaw_accel(
320
+ states: npt.NDArray[np.float64],
321
+ time_steps_s: npt.NDArray[np.float64],
322
+ vehicle_parameters: VehicleParameters,
323
+ ) -> npt.NDArray[np.bool_]:
324
+ """
325
+ Compute acceleration of yaw-angle over batch-dim of simulated proposals
326
+ :param states: array representation of ego state values
327
+ :param time_steps_s: time steps [s] of time dim
328
+ :param vehicle_parameters: parameters of vehicle
329
+ :return: acceleration of yaw-angle within bound
330
+ """
331
+ yaw_accel_metric = _extract_ego_yaw_rate(states, time_steps_s, deriv_order=2, poly_order=3)
332
+ return _within_bound(yaw_accel_metric, min_bound=-MAX_ABS_YAW_ACCEL, max_bound=MAX_ABS_YAW_ACCEL)
333
+
334
+
335
+ def _compute_yaw_rate(
336
+ states: npt.NDArray[np.float64],
337
+ time_steps_s: npt.NDArray[np.float64],
338
+ vehicle_parameters: VehicleParameters,
339
+ ) -> npt.NDArray[np.bool_]:
340
+ """
341
+ Compute velocity of yaw-angle over batch-dim of simulated proposals
342
+ :param states: array representation of ego state values
343
+ :param time_steps_s: time steps [s] of time dim
344
+ :param vehicle_parameters: parameters of vehicle
345
+ :return: velocity of yaw-angle within bound
346
+ """
347
+ yaw_rate_metric = _extract_ego_yaw_rate(states, time_steps_s)
348
+ return _within_bound(yaw_rate_metric, min_bound=-MAX_ABS_YAW_RATE, max_bound=MAX_ABS_YAW_RATE)
349
+
350
+
351
+ def ego_is_comfortable(
352
+ states: npt.NDArray[np.float64],
353
+ time_point_s: npt.NDArray[np.float64],
354
+ vehicle_parameters: VehicleParameters = get_pacifica_parameters(),
355
+ ) -> npt.NDArray[np.bool_]:
356
+ """
357
+ Accumulates all within-bound comfortability metrics
358
+ :param states: array representation of ego state values
359
+ :param time_point_s: time steps [s] of time dim
360
+ :param vehicle_parameters: parameters of vehicle
361
+ :return: _description_
362
+ """
363
+ n_batch, n_time, n_states = states.shape
364
+ assert n_time == len(time_point_s)
365
+ assert n_states == StateIndex.size()
366
+
367
+ comfort_metric_functions = [
368
+ _compute_lon_acceleration,
369
+ _compute_lat_acceleration,
370
+ _compute_jerk_metric,
371
+ _compute_lon_jerk_metric,
372
+ _compute_yaw_accel,
373
+ _compute_yaw_rate,
374
+ ]
375
+ results: npt.NDArray[np.bool_] = np.zeros((n_batch, len(comfort_metric_functions)), dtype=np.bool_)
376
+ for idx, metric_function in enumerate(comfort_metric_functions):
377
+ results[:, idx] = metric_function(states, time_point_s, vehicle_parameters)
378
+
379
+ return results
380
+
381
+
382
+ def calculate_rms_difference(
383
+ feature_values: npt.NDArray[np.float64],
384
+ ) -> npt.NDArray[np.float64]:
385
+ """
386
+ Calculate RMS difference between consecutive frames for a given feature.
387
+ :param feature_values: Array of shape (n_batch, n_time) containing feature values for each time step.
388
+ :return: RMS differences for each trajectory in the batch.
389
+ """
390
+ differences = np.diff(feature_values, axis=1) # Calculate frame-to-frame differences
391
+ squared_differences = differences**2 # Square the differences
392
+ mean_squared_diff = np.mean(squared_differences, axis=1) # Take the mean over time
393
+ rms = np.sqrt(mean_squared_diff) # Compute the square root to get RMS
394
+ return rms
395
+
396
+
397
+ def calculate_rms(values: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
398
+ """
399
+ Compute the Root Mean Square (RMS) of the given values along the time axis.
400
+
401
+ :param values: Array containing values (n_batch, n_time).
402
+ :return: RMS value per batch (n_batch,).
403
+ """
404
+ squared_values = values**2 # Square the differences
405
+ mean_squared = np.mean(squared_values, axis=1) # Compute mean along time axis
406
+ rms_values = np.sqrt(mean_squared) # Square root to get RMS
407
+ return rms_values
408
+
409
+
410
+ def extract_features(
411
+ states: npt.NDArray[np.float64],
412
+ time_point_s: npt.NDArray[np.float64],
413
+ vehicle_parameters: VehicleParameters = get_pacifica_parameters(),
414
+ ) -> dict:
415
+ """
416
+ Extract features needed for Extended Comfort evaluation.
417
+ :param states: Array of ego states (n_batch, n_time, n_features).
418
+ :param time_point_s: Array of time steps in seconds.
419
+ :param vehicle_parameters: parameters of vehicle
420
+ :return: A dictionary of features.
421
+ """
422
+ return {
423
+ "acceleration": _extract_ego_acceleration(states, "magnitude", vehicle_parameters=vehicle_parameters),
424
+ "jerk": _extract_ego_jerk(states, "magnitude", time_point_s, vehicle_parameters=vehicle_parameters),
425
+ "yaw_rate": _extract_ego_yaw_rate(states, time_point_s),
426
+ "yaw_accel": _extract_ego_yaw_rate(states, time_point_s, deriv_order=2),
427
+ }
428
+
429
+
430
+ def ego_is_two_frame_extended_comfort(
431
+ states_1: npt.NDArray[np.float64],
432
+ states_2: npt.NDArray[np.float64],
433
+ time_point_s: npt.NDArray[np.float64],
434
+ ) -> npt.NDArray[np.bool_]:
435
+ """
436
+ Evaluate whether the differences between two consecutive trajectories satisfy the Extended Comfort metric.
437
+ :param states_1: First trajectory (n_batch, n_time, n_features).
438
+ :param states_2: Second trajectory (n_batch, n_time, n_features).
439
+ :param time_point_s: Array of time steps in seconds.
440
+ :return: Boolean array indicating whether the difference between trajectories meets the criteria.
441
+ """
442
+ assert states_1.shape == states_2.shape, "Both trajectories must have the same shape"
443
+
444
+ # Extract features for both trajectories
445
+ features_1 = extract_features(states_1, time_point_s)
446
+ features_2 = extract_features(states_2, time_point_s)
447
+
448
+ # Compute differences between corresponding time steps
449
+ diff_acceleration = features_1["acceleration"] - features_2["acceleration"]
450
+ diff_jerk = features_1["jerk"] - features_2["jerk"]
451
+ diff_yaw_rate = features_1["yaw_rate"] - features_2["yaw_rate"]
452
+ diff_yaw_accel = features_1["yaw_accel"] - features_2["yaw_accel"]
453
+
454
+ # Calculate RMS differences
455
+ rms_acceleration = calculate_rms(diff_acceleration)
456
+ rms_jerk = calculate_rms(diff_jerk)
457
+ rms_yaw_rate = calculate_rms(diff_yaw_rate)
458
+ rms_yaw_accel = calculate_rms(diff_yaw_accel)
459
+
460
+ # Compare RMS differences against thresholds
461
+ meets_acceleration = rms_acceleration <= acceleration_threshold
462
+ meets_jerk = rms_jerk <= jerk_threshold
463
+ meets_yaw_rate = rms_yaw_rate <= yaw_rate_threshold
464
+ meets_yaw_accel = rms_yaw_accel <= yaw_accel_threshold
465
+
466
+ # Combine all criteria
467
+ return np.logical_and.reduce([meets_acceleration, meets_jerk, meets_yaw_rate, meets_yaw_accel])
navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_scorer.py ADDED
@@ -0,0 +1,700 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from dataclasses import dataclass
3
+ from typing import List, Optional
4
+
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+ import pandas as pd
8
+ from nuplan.common.actor_state.state_representation import StateSE2, TimePoint
9
+ from nuplan.common.actor_state.tracked_objects_types import AGENT_TYPES
10
+ from nuplan.common.actor_state.vehicle_parameters import VehicleParameters, get_pacifica_parameters
11
+ from nuplan.common.maps.maps_datatypes import SemanticMapLayer
12
+ from nuplan.planning.metrics.utils.collision_utils import CollisionType
13
+ from nuplan.planning.simulation.observation.idm.utils import is_agent_ahead, is_agent_behind
14
+ from nuplan.planning.simulation.observation.observation_type import DetectionsTracks
15
+ from nuplan.planning.simulation.trajectory.interpolated_trajectory import InterpolatedTrajectory
16
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
17
+ from shapely import Point, creation
18
+
19
+ from navsim.common.dataclasses import PDMResults
20
+ from navsim.planning.metric_caching.metric_cache import MapParameters
21
+ from navsim.planning.simulation.planner.pdm_planner.observation.pdm_observation import PDMObservation
22
+ from navsim.planning.simulation.planner.pdm_planner.observation.pdm_occupancy_map import PDMDrivableMap
23
+ from navsim.planning.simulation.planner.pdm_planner.scoring.pdm_comfort_metrics import ego_is_comfortable
24
+ from navsim.planning.simulation.planner.pdm_planner.scoring.pdm_scorer_utils import get_collision_type
25
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_array_representation import (
26
+ coords_array_to_polygon_array,
27
+ ego_states_to_state_array,
28
+ state_array_to_coords_array,
29
+ )
30
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import (
31
+ BBCoordsIndex,
32
+ EgoAreaIndex,
33
+ MultiMetricIndex,
34
+ StateIndex,
35
+ WeightedMetricIndex,
36
+ )
37
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_path import PDMPath
38
+
39
+
40
+ @dataclass
41
+ class PDMScorerConfig:
42
+
43
+ # weighted metric weights
44
+ progress_weight: float = 5.0
45
+ ttc_weight: float = 5.0
46
+ lane_keeping_weight: float = 2.0
47
+ history_comfort_weight: float = 2.0
48
+ two_frame_extended_comfort_weight: float = 2.0
49
+
50
+ # thresholds
51
+ # comfort related config in navsim/planning/simulation/planner/pdm_planner/scoring/pdm_comfort_metrics.py
52
+ driving_direction_horizon: float = 1.0 # [s] (driving direction) (nuplan)
53
+ driving_direction_compliance_threshold: float = 2.0 # [m] (driving direction) (nuplan)
54
+ driving_direction_violation_threshold: float = 6.0 # [m] (driving direction) (nuplan)
55
+
56
+ stopped_speed_threshold: float = 5e-03 # [m/s] (ttc)
57
+ future_collision_horizon_window: float = 1.0 # [s] (ttc)
58
+ progress_distance_threshold: float = 5.0 # [m] (progress)
59
+ lane_keeping_deviation_limit: float = 0.5 # [m] (lane keeping) (hydraMDP++)
60
+ lane_keeping_horizon_window: float = 2.0 # [s] (lane keeping) (hydraMDP++)
61
+
62
+ # human flag
63
+ human_penalty_filter: Optional[bool] = None
64
+
65
+ @property
66
+ def weighted_metrics_array(self) -> npt.NDArray[np.float64]:
67
+ weighted_metrics = np.zeros(len(WeightedMetricIndex), dtype=np.float64)
68
+ weighted_metrics[WeightedMetricIndex.PROGRESS] = self.progress_weight
69
+ weighted_metrics[WeightedMetricIndex.TTC] = self.ttc_weight
70
+ weighted_metrics[WeightedMetricIndex.LANE_KEEPING] = self.lane_keeping_weight
71
+ weighted_metrics[WeightedMetricIndex.HISTORY_COMFORT] = self.history_comfort_weight
72
+ weighted_metrics[WeightedMetricIndex.TWO_FRAME_EXTENDED_COMFORT] = self.two_frame_extended_comfort_weight
73
+ return weighted_metrics
74
+
75
+
76
+ class PDMScorer:
77
+ """Class to score proposals in PDM pipeline. Re-implements nuPlan's closed-loop metrics."""
78
+
79
+ def __init__(
80
+ self,
81
+ proposal_sampling: TrajectorySampling,
82
+ config: PDMScorerConfig = PDMScorerConfig(),
83
+ vehicle_parameters: VehicleParameters = get_pacifica_parameters(),
84
+ ):
85
+ """
86
+ Constructor of PDMScorer
87
+ :param proposal_sampling: Sampling parameters for proposals
88
+ """
89
+ self.proposal_sampling = proposal_sampling
90
+ self._config = config
91
+ self._vehicle_parameters = vehicle_parameters
92
+
93
+ # lazy loaded
94
+ self._observation: Optional[PDMObservation] = None
95
+ self._centerline: Optional[PDMPath] = None
96
+ self._route_lane_ids: Optional[List[str]] = None
97
+ self._drivable_area_map: Optional[PDMDrivableMap] = None
98
+ self._human_past_trajectory: Optional[InterpolatedTrajectory] = None
99
+
100
+ self._num_proposals: Optional[int] = None
101
+ self._states: Optional[npt.NDArray[np.float64]] = None
102
+ self._ego_coords: Optional[npt.NDArray[np.float64]] = None
103
+ self._ego_polygons: Optional[npt.NDArray[np.object_]] = None
104
+
105
+ self._ego_areas: Optional[npt.NDArray[np.bool_]] = None
106
+
107
+ self._multi_metrics: Optional[npt.NDArray[np.float64]] = None
108
+ self._weighted_metrics: Optional[npt.NDArray[np.float64]] = None
109
+ self._progress_raw: Optional[npt.NDArray[np.float64]] = None
110
+
111
+ self._collision_time_idcs: Optional[npt.NDArray[np.float64]] = None
112
+ self._ttc_time_idcs: Optional[npt.NDArray[np.float64]] = None
113
+
114
+ def time_to_at_fault_collision(self, proposal_idx: int) -> float:
115
+ """
116
+ Returns time to at-fault collision for given proposal
117
+ :param proposal_idx: index for proposal
118
+ :return: time to infraction
119
+ """
120
+ return self._collision_time_idcs[proposal_idx] * self.proposal_sampling.interval_length
121
+
122
+ def time_to_ttc_infraction(self, proposal_idx: int) -> float:
123
+ """
124
+ Returns time to ttc infraction for given proposal
125
+ :param proposal_idx: index for proposal
126
+ :return: time to infraction
127
+ """
128
+ return self._ttc_time_idcs[proposal_idx] * self.proposal_sampling.interval_length
129
+
130
+ def score_proposals(
131
+ self,
132
+ states: npt.NDArray[np.float64],
133
+ observation: PDMObservation,
134
+ centerline: PDMPath,
135
+ route_lane_ids: List[str],
136
+ drivable_area_map: PDMDrivableMap,
137
+ map_parameters: Optional[MapParameters] = None,
138
+ simulated_agent_detections_tracks: Optional[List[DetectionsTracks]] = None,
139
+ human_past_trajectory: Optional[InterpolatedTrajectory] = None,
140
+ ) -> List[pd.DataFrame]:
141
+ """
142
+ TODO: Update this docstring
143
+ Scores proposal similar to nuPlan's closed-loop metrics
144
+ :param states: array representation of simulated proposals
145
+ :param observation: PDM's observation class
146
+ :param centerline: path of the centerline
147
+ :param route_lane_ids: list containing on-route lane ids
148
+ :param drivable_area_map: Occupancy map of drivable are polygons
149
+ :return: A List containing the PDMResult for each proposal
150
+ """
151
+ if simulated_agent_detections_tracks is not None:
152
+ observation.update_detections_tracks(
153
+ detection_tracks=simulated_agent_detections_tracks,
154
+ )
155
+
156
+ # initialize & lazy load class values
157
+ self._reset(
158
+ states,
159
+ observation,
160
+ centerline,
161
+ route_lane_ids,
162
+ drivable_area_map,
163
+ human_past_trajectory,
164
+ )
165
+
166
+ # fill value ego-area array (used in multiple metrics)
167
+ self._calculate_ego_area()
168
+
169
+ # 1. multiplicative metrics
170
+ self._calculate_no_at_fault_collision()
171
+ self._calculate_drivable_area_compliance()
172
+ self._calculate_traffic_light_compliance()
173
+ self._calculate_driving_direction_compliance()
174
+
175
+ # 2. weighted metrics
176
+ self._calculate_progress()
177
+ self._calculate_ttc()
178
+ self._calculate_lane_keeping()
179
+ self._calculate_history_comfort()
180
+
181
+ pdm_scores = self._aggregate_pdm_scores()
182
+ multiplicative_metrics_prods, weighted_metrics_all = self._multi_metrics.prod(axis=0), self._weighted_metrics
183
+
184
+ results: List[pd.DataFrame] = []
185
+ for proposal_idx in range(self._num_proposals):
186
+
187
+ no_at_fault_collisions = self._multi_metrics[MultiMetricIndex.NO_COLLISION, proposal_idx]
188
+ drivable_area_compliance = self._multi_metrics[MultiMetricIndex.DRIVABLE_AREA, proposal_idx]
189
+ driving_direction_compliance = self._multi_metrics[MultiMetricIndex.DRIVING_DIRECTION, proposal_idx]
190
+ traffic_light_compliance = self._multi_metrics[MultiMetricIndex.TRAFFIC_LIGHT_COMPLIANCE, proposal_idx]
191
+
192
+ ego_progress = self._weighted_metrics[WeightedMetricIndex.PROGRESS, proposal_idx]
193
+ time_to_collision_within_bound = self._weighted_metrics[WeightedMetricIndex.TTC, proposal_idx]
194
+ lane_keeping = self._weighted_metrics[WeightedMetricIndex.LANE_KEEPING, proposal_idx]
195
+ history_comfort = self._weighted_metrics[WeightedMetricIndex.HISTORY_COMFORT, proposal_idx]
196
+
197
+ multiplicative_metrics_prod = multiplicative_metrics_prods[proposal_idx]
198
+ weighted_metrics = weighted_metrics_all[:, proposal_idx]
199
+ pdm_score = pdm_scores[proposal_idx]
200
+
201
+ results.append(
202
+ pd.DataFrame(
203
+ [
204
+ PDMResults(
205
+ no_at_fault_collisions=no_at_fault_collisions,
206
+ drivable_area_compliance=drivable_area_compliance,
207
+ driving_direction_compliance=driving_direction_compliance,
208
+ traffic_light_compliance=traffic_light_compliance,
209
+ ego_progress=ego_progress,
210
+ time_to_collision_within_bound=time_to_collision_within_bound,
211
+ lane_keeping=lane_keeping,
212
+ history_comfort=history_comfort,
213
+ multiplicative_metrics_prod=multiplicative_metrics_prod,
214
+ weighted_metrics=weighted_metrics,
215
+ weighted_metrics_array=self._config.weighted_metrics_array,
216
+ pdm_score=pdm_score,
217
+ )
218
+ ]
219
+ )
220
+ )
221
+ return results
222
+
223
+ def _aggregate_pdm_scores(self) -> npt.NDArray[np.float64]:
224
+ """
225
+ Score for PDM proposals, ignoring two-frame extended comfort.
226
+ """
227
+
228
+ # accumulate multiplicative metrics
229
+ multiplicate_metric_scores = self._multi_metrics.prod(axis=0)
230
+
231
+ # normalize and fill progress values
232
+ masked_progress = self._progress_raw * multiplicate_metric_scores
233
+ norm_constant_progress = np.max(masked_progress)
234
+ if norm_constant_progress > self._config.progress_distance_threshold:
235
+ normalized_progress = np.clip(self._progress_raw / norm_constant_progress, 0.0, 1.0)
236
+ else:
237
+ normalized_progress = np.ones(len(masked_progress), dtype=np.float64)
238
+ self._weighted_metrics[WeightedMetricIndex.PROGRESS] = normalized_progress
239
+
240
+ # Exclude the two-frame extended comfort metric from the weighted metrics calculation.
241
+ mask = np.ones_like(self._config.weighted_metrics_array, dtype=bool)
242
+ mask[WeightedMetricIndex.TWO_FRAME_EXTENDED_COMFORT] = False
243
+
244
+ weighted_metrics_array = self._config.weighted_metrics_array
245
+ weighted_metric_scores = (self._weighted_metrics[mask] * weighted_metrics_array[mask, None]).sum(axis=0)
246
+ weighted_metric_scores /= weighted_metrics_array[mask].sum()
247
+
248
+ # calculate final scores
249
+ final_scores = multiplicate_metric_scores * weighted_metric_scores
250
+
251
+ return final_scores
252
+
253
+ def _reset(
254
+ self,
255
+ states: npt.NDArray[np.float64],
256
+ observation: PDMObservation,
257
+ centerline: PDMPath,
258
+ route_lane_ids: List[str],
259
+ drivable_area_map: PDMDrivableMap,
260
+ human_past_trajectory: Optional[InterpolatedTrajectory],
261
+ ) -> None:
262
+ """
263
+ Resets metric values and lazy loads input classes.
264
+ :param states: array representation of simulated proposals
265
+ :param observation: PDM's observation class
266
+ :param centerline: path of the centerline
267
+ :param route_lane_ids: list containing on-route lane ids
268
+ :param drivable_area_map: Occupancy map of drivable are polygons
269
+ """
270
+ assert states.ndim == 3
271
+ assert states.shape[1] == self.proposal_sampling.num_poses + 1
272
+ assert states.shape[2] == StateIndex.size()
273
+
274
+ self._observation = observation
275
+ self._centerline = centerline
276
+ self._route_lane_ids = route_lane_ids
277
+ self._drivable_area_map = drivable_area_map
278
+ self._human_past_trajectory = human_past_trajectory
279
+
280
+ self._num_proposals = states.shape[0]
281
+
282
+ # save ego state values
283
+ self._states = states
284
+
285
+ # calculate coordinates of ego corners and center
286
+ self._ego_coords = state_array_to_coords_array(states, self._vehicle_parameters)
287
+
288
+ # initialize all ego polygons from corners
289
+ self._ego_polygons = coords_array_to_polygon_array(self._ego_coords)
290
+
291
+ # zero initialize all remaining arrays.
292
+ self._ego_areas = np.zeros(
293
+ (
294
+ self._num_proposals,
295
+ self.proposal_sampling.num_poses + 1,
296
+ len(EgoAreaIndex),
297
+ ),
298
+ dtype=np.bool_,
299
+ )
300
+ self._multi_metrics = np.zeros((len(MultiMetricIndex), self._num_proposals), dtype=np.float64)
301
+ self._weighted_metrics = np.zeros((len(WeightedMetricIndex), self._num_proposals), dtype=np.float64)
302
+ self._progress_raw = np.zeros(self._num_proposals, dtype=np.float64)
303
+
304
+ # initialize infraction arrays with infinity (meaning no infraction occurs)
305
+ self._collision_time_idcs = np.zeros(self._num_proposals, dtype=np.float64)
306
+ self._ttc_time_idcs = np.zeros(self._num_proposals, dtype=np.float64)
307
+ self._collision_time_idcs.fill(np.inf)
308
+ self._ttc_time_idcs.fill(np.inf)
309
+
310
+ def _calculate_ego_area(self) -> None:
311
+ """
312
+ Determines the area of proposals over time.
313
+ Areas are (1) in multiple lanes, (2) non-drivable area, or (3) oncoming traffic
314
+ """
315
+
316
+ n_proposals, n_horizon, n_points, _ = self._ego_coords.shape
317
+
318
+ in_polygons = self._drivable_area_map.points_in_polygons(self._ego_coords)
319
+ in_polygons = in_polygons.transpose(1, 2, 0, 3) # shape: n_proposals, n_horizon, n_polygons, n_points
320
+
321
+ drivable_area_idcs = self._drivable_area_map.get_indices_of_map_type(
322
+ [
323
+ SemanticMapLayer.ROADBLOCK,
324
+ SemanticMapLayer.INTERSECTION,
325
+ SemanticMapLayer.DRIVABLE_AREA,
326
+ SemanticMapLayer.CARPARK_AREA,
327
+ ]
328
+ )
329
+
330
+ drivable_lane_idcs = self._drivable_area_map.get_indices_of_map_type(
331
+ [SemanticMapLayer.LANE, SemanticMapLayer.LANE_CONNECTOR]
332
+ )
333
+
334
+ drivable_on_route_idcs: List[int] = [
335
+ idx for idx in drivable_lane_idcs if self._drivable_area_map.tokens[idx] in self._route_lane_ids
336
+ ] # index mask for on-route lanes
337
+
338
+ corners_in_polygon = in_polygons[..., :-1] # ignore center coordinate
339
+ center_in_polygon = in_polygons[..., -1] # only center
340
+
341
+ # in_multiple_lanes: if
342
+ # - more than one drivable polygon contains at least one corner
343
+ # - no polygon contains all corners
344
+ batch_multiple_lanes_mask = np.zeros((n_proposals, n_horizon), dtype=np.bool_)
345
+ batch_multiple_lanes_mask = (corners_in_polygon[:, :, drivable_lane_idcs].sum(axis=-1) > 0).sum(axis=-1) > 1
346
+
347
+ batch_not_single_lanes_mask = np.zeros((n_proposals, n_horizon), dtype=np.bool_)
348
+ batch_not_single_lanes_mask = np.all(corners_in_polygon[:, :, drivable_lane_idcs].sum(axis=-1) != 4, axis=-1)
349
+
350
+ multiple_lanes_mask = np.logical_and(batch_multiple_lanes_mask, batch_not_single_lanes_mask)
351
+ self._ego_areas[multiple_lanes_mask, EgoAreaIndex.MULTIPLE_LANES] = True
352
+
353
+ # in_nondrivable_area: if at least one corner is not within any drivable polygon
354
+ batch_nondrivable_area_mask = np.zeros((n_proposals, n_horizon), dtype=np.bool_)
355
+ batch_nondrivable_area_mask = (corners_in_polygon[:, :, drivable_area_idcs].sum(axis=-2) > 0).sum(axis=-1) < 4
356
+ self._ego_areas[batch_nondrivable_area_mask, EgoAreaIndex.NON_DRIVABLE_AREA] = True
357
+
358
+ # in_oncoming_traffic: if center not in any drivable polygon that is on-route
359
+ batch_oncoming_traffic_mask = np.zeros((n_proposals, n_horizon), dtype=np.bool_)
360
+ batch_oncoming_traffic_mask = center_in_polygon[..., drivable_on_route_idcs].sum(axis=-1) == 0
361
+ self._ego_areas[batch_oncoming_traffic_mask, EgoAreaIndex.ONCOMING_TRAFFIC] = True
362
+
363
+ def _calculate_no_at_fault_collision(self) -> None:
364
+ """
365
+ Re-implementation of nuPlan's at-fault collision metric.
366
+ """
367
+ no_at_fault_collision_scores = np.ones(self._num_proposals, dtype=np.float64)
368
+
369
+ proposal_collided_track_ids = {
370
+ proposal_idx: copy.deepcopy(self._observation.collided_track_ids)
371
+ for proposal_idx in range(self._num_proposals)
372
+ }
373
+
374
+ for time_idx in range(self.proposal_sampling.num_poses + 1):
375
+ ego_polygons = self._ego_polygons[:, time_idx]
376
+ intersecting = self._observation[time_idx].query(ego_polygons, predicate="intersects")
377
+
378
+ if len(intersecting) == 0:
379
+ continue
380
+
381
+ for proposal_idx, geometry_idx in zip(intersecting[0], intersecting[1]):
382
+ token = self._observation[time_idx].tokens[geometry_idx]
383
+ if (self._observation.red_light_token in token) or (token in proposal_collided_track_ids[proposal_idx]):
384
+ continue
385
+
386
+ ego_in_multiple_lanes_or_nondrivable_area = (
387
+ self._ego_areas[proposal_idx, time_idx, EgoAreaIndex.MULTIPLE_LANES]
388
+ or self._ego_areas[proposal_idx, time_idx, EgoAreaIndex.NON_DRIVABLE_AREA]
389
+ )
390
+
391
+ tracked_object = self._observation.unique_objects[token]
392
+
393
+ # classify collision
394
+ collision_type: CollisionType = get_collision_type(
395
+ self._states[proposal_idx, time_idx],
396
+ self._ego_polygons[proposal_idx, time_idx],
397
+ tracked_object,
398
+ self._observation[time_idx][token],
399
+ )
400
+ collisions_at_stopped_track_or_active_front: bool = collision_type in [
401
+ CollisionType.ACTIVE_FRONT_COLLISION,
402
+ CollisionType.STOPPED_TRACK_COLLISION,
403
+ ]
404
+ collision_at_lateral: bool = collision_type == CollisionType.ACTIVE_LATERAL_COLLISION
405
+
406
+ # 1. at fault collision
407
+ if collisions_at_stopped_track_or_active_front or (
408
+ ego_in_multiple_lanes_or_nondrivable_area and collision_at_lateral
409
+ ):
410
+ no_at_fault_collision_score = 0.0 if tracked_object.tracked_object_type in AGENT_TYPES else 0.5
411
+ no_at_fault_collision_scores[proposal_idx] = np.minimum(
412
+ no_at_fault_collision_scores[proposal_idx],
413
+ no_at_fault_collision_score,
414
+ )
415
+ self._collision_time_idcs[proposal_idx] = min(time_idx, self._collision_time_idcs[proposal_idx])
416
+
417
+ else: # 2. no at fault collision
418
+ proposal_collided_track_ids[proposal_idx].append(token)
419
+
420
+ self._multi_metrics[MultiMetricIndex.NO_COLLISION] = no_at_fault_collision_scores
421
+
422
+ def _calculate_drivable_area_compliance(self) -> None:
423
+ """
424
+ Re-implementation of nuPlan's drivable area compliance metric
425
+ """
426
+ drivable_area_compliance_scores = np.ones(self._num_proposals, dtype=np.float64)
427
+ off_road_mask = self._ego_areas[:, :, EgoAreaIndex.NON_DRIVABLE_AREA].any(axis=-1)
428
+ drivable_area_compliance_scores[off_road_mask] = 0.0
429
+ self._multi_metrics[MultiMetricIndex.DRIVABLE_AREA] = drivable_area_compliance_scores
430
+
431
+ def _calculate_driving_direction_compliance(self) -> None:
432
+ """
433
+ Re-implementation of nuPlan's driving direction compliance metric
434
+ """
435
+ center_coordinates = self._ego_coords[:, :, BBCoordsIndex.CENTER]
436
+ oncoming_progress = np.zeros(
437
+ (self._num_proposals, self.proposal_sampling.num_poses + 1),
438
+ dtype=np.float64,
439
+ )
440
+ oncoming_progress[:, 1:] = np.linalg.norm(center_coordinates[:, 1:] - center_coordinates[:, :-1], axis=-1)
441
+
442
+ # mask out points that are not in oncoming traffic
443
+ oncoming_traffic_masks = self._ego_areas[:, :, EgoAreaIndex.ONCOMING_TRAFFIC]
444
+
445
+ # remove intersection
446
+ for proposal_idx in range(self._num_proposals):
447
+ for time_idx in range(self.proposal_sampling.num_poses + 1):
448
+ ego_position = Point(*center_coordinates[proposal_idx, time_idx])
449
+ is_in_intersection = self._drivable_area_map.is_in_layer(ego_position, SemanticMapLayer.INTERSECTION)
450
+ if not oncoming_traffic_masks[proposal_idx, time_idx] or is_in_intersection:
451
+ oncoming_progress[proposal_idx, time_idx] = 0.0
452
+
453
+ # aggregate
454
+ driving_direction_compliance_scores = np.ones(self._num_proposals, dtype=np.float64)
455
+ horizon = int(self._config.driving_direction_horizon / self.proposal_sampling.interval_length)
456
+
457
+ oncoming_progress_over_horizon = np.concatenate(
458
+ [
459
+ oncoming_progress[:, max(0, time_idx - horizon) : time_idx + 1].sum(axis=-1)[..., None]
460
+ for time_idx in range(oncoming_progress.shape[-1])
461
+ ],
462
+ dtype=np.float64,
463
+ axis=-1,
464
+ )
465
+
466
+ for proposal_idx, progress in enumerate(oncoming_progress_over_horizon.max(axis=-1)):
467
+ if progress < self._config.driving_direction_compliance_threshold:
468
+ driving_direction_compliance_scores[proposal_idx] = 1.0
469
+ elif progress < self._config.driving_direction_violation_threshold:
470
+ driving_direction_compliance_scores[proposal_idx] = 0.5
471
+ else:
472
+ driving_direction_compliance_scores[proposal_idx] = 0.0
473
+
474
+ self._multi_metrics[MultiMetricIndex.DRIVING_DIRECTION] = driving_direction_compliance_scores
475
+
476
+ def _calculate_progress(self) -> None:
477
+ """
478
+ Re-implementation of nuPlan's progress metric (non-normalized).
479
+ Calculates progress along the centerline.
480
+ """
481
+
482
+ # calculate raw progress in meter
483
+ progress_in_meter = np.zeros(self._num_proposals, dtype=np.float64)
484
+ for proposal_idx in range(self._num_proposals):
485
+ start_point = Point(*self._ego_coords[proposal_idx, 0, BBCoordsIndex.CENTER])
486
+ end_point = Point(*self._ego_coords[proposal_idx, -1, BBCoordsIndex.CENTER])
487
+ progress = self._centerline.project([start_point, end_point])
488
+ progress_in_meter[proposal_idx] = progress[1] - progress[0]
489
+
490
+ self._progress_raw = np.clip(progress_in_meter, a_min=0, a_max=None)
491
+
492
+ def _calculate_ttc(self):
493
+ """
494
+ Re-implementation of nuPlan's time-to-collision metric.
495
+ """
496
+
497
+ ttc_scores = np.ones(self._num_proposals, dtype=np.float64)
498
+ temp_collided_track_ids = {
499
+ proposal_idx: copy.deepcopy(self._observation.collided_track_ids)
500
+ for proposal_idx in range(self._num_proposals)
501
+ }
502
+
503
+ # calculate TTC for specific time horizon (default:1s) in the future with less temporal resolution.
504
+ future_time_idcs = np.arange(0, int(self._config.future_collision_horizon_window * 10), 3)
505
+ n_future_steps = len(future_time_idcs)
506
+
507
+ # create polygons for each ego position and specific time horizon (default:1s) future projection
508
+ coords_exterior = self._ego_coords.copy()
509
+ coords_exterior[:, :, BBCoordsIndex.CENTER, :] = coords_exterior[:, :, BBCoordsIndex.FRONT_LEFT, :]
510
+ coords_exterior_time_steps = np.repeat(coords_exterior[:, :, None], n_future_steps, axis=2)
511
+
512
+ speeds = np.hypot(
513
+ self._states[..., StateIndex.VELOCITY_X],
514
+ self._states[..., StateIndex.VELOCITY_Y],
515
+ )
516
+
517
+ dxy_per_s = np.stack(
518
+ [
519
+ np.cos(self._states[..., StateIndex.HEADING]) * speeds,
520
+ np.sin(self._states[..., StateIndex.HEADING]) * speeds,
521
+ ],
522
+ axis=-1,
523
+ )
524
+
525
+ for idx, future_time_idx in enumerate(future_time_idcs):
526
+ delta_t = float(future_time_idx) * self.proposal_sampling.interval_length
527
+ coords_exterior_time_steps[:, :, idx] = (
528
+ coords_exterior_time_steps[:, :, idx] + dxy_per_s[:, :, None] * delta_t
529
+ )
530
+
531
+ polygons = creation.polygons(coords_exterior_time_steps)
532
+
533
+ # ttc needs to look future_time_idcs into the future,
534
+ # so we can only calculate it for n_proposal_steps_to_evaluate steps
535
+
536
+ n_proposal_steps_to_evaluate = self.proposal_sampling.num_poses - max(future_time_idcs)
537
+ # check collision for each proposal and projection
538
+ for time_idx in range(n_proposal_steps_to_evaluate + 1):
539
+ for step_idx, future_time_idx in enumerate(future_time_idcs):
540
+ current_time_idx = time_idx + future_time_idx
541
+ polygons_at_time_step = polygons[:, time_idx, step_idx]
542
+ intersecting = self._observation[current_time_idx].query(polygons_at_time_step, predicate="intersects")
543
+
544
+ if len(intersecting) == 0:
545
+ continue
546
+
547
+ for proposal_idx, geometry_idx in zip(intersecting[0], intersecting[1]):
548
+ token = self._observation[current_time_idx].tokens[geometry_idx]
549
+ if (
550
+ (self._observation.red_light_token in token)
551
+ or (token in temp_collided_track_ids[proposal_idx])
552
+ or (speeds[proposal_idx, time_idx] < self._config.stopped_speed_threshold)
553
+ ):
554
+ continue
555
+
556
+ ego_in_multiple_lanes_or_nondrivable_area = (
557
+ self._ego_areas[proposal_idx, time_idx, EgoAreaIndex.MULTIPLE_LANES]
558
+ or self._ego_areas[proposal_idx, time_idx, EgoAreaIndex.NON_DRIVABLE_AREA]
559
+ )
560
+ ego_rear_axle: StateSE2 = StateSE2(*self._states[proposal_idx, time_idx, StateIndex.STATE_SE2])
561
+
562
+ centroid = self._observation[current_time_idx][token].centroid
563
+ track_heading = self._observation.unique_objects[token].box.center.heading
564
+ track_state = StateSE2(centroid.x, centroid.y, track_heading)
565
+ # TODO: fix ego_area for intersection
566
+ if is_agent_ahead(ego_rear_axle, track_state) or (
567
+ (
568
+ ego_in_multiple_lanes_or_nondrivable_area
569
+ or self._drivable_area_map.is_in_layer(
570
+ ego_rear_axle.point, layer=SemanticMapLayer.INTERSECTION
571
+ )
572
+ )
573
+ and not is_agent_behind(ego_rear_axle, track_state)
574
+ ):
575
+ ttc_scores[proposal_idx] = np.minimum(ttc_scores[proposal_idx], 0.0)
576
+ self._ttc_time_idcs[proposal_idx] = min(time_idx, self._ttc_time_idcs[proposal_idx])
577
+ else:
578
+ temp_collided_track_ids[proposal_idx].append(token)
579
+
580
+ self._weighted_metrics[WeightedMetricIndex.TTC] = ttc_scores
581
+
582
+ def _calculate_traffic_light_compliance(self) -> None:
583
+ """
584
+ Re-implementation of hydraMDP++'s traffic light compliance metric.
585
+ """
586
+ # Initialize scores for all proposals to 1 (compliant by default)
587
+ traffic_light_compliance_scores = np.ones(self._num_proposals, dtype=np.float64)
588
+
589
+ # Iterate over each time step within the horizon
590
+ for time_idx in range(self.proposal_sampling.num_poses + 1):
591
+ # Get ego polygons (vehicle shapes) at the current time step
592
+ ego_polygons = self._ego_polygons[:, time_idx]
593
+ # Query objects intersecting with the ego polygons
594
+ intersecting = self._observation[time_idx].query(ego_polygons, predicate="intersects")
595
+
596
+ # If no intersections, skip this time step
597
+ if len(intersecting) == 0:
598
+ continue
599
+
600
+ # Iterate over each intersecting object
601
+ for proposal_idx, geometry_idx in zip(intersecting[0], intersecting[1]):
602
+ # Skip if the score is already 0
603
+ if traffic_light_compliance_scores[proposal_idx] == 0.0:
604
+ continue
605
+
606
+ token = self._observation[time_idx].tokens[geometry_idx]
607
+
608
+ # Check if the intersecting object is a red light
609
+ if token.startswith(self._observation.red_light_token):
610
+ traffic_light_compliance_scores[proposal_idx] = 0.0
611
+
612
+ # Store the scores in the multi-metrics system for later evaluation
613
+ self._multi_metrics[MultiMetricIndex.TRAFFIC_LIGHT_COMPLIANCE] = traffic_light_compliance_scores
614
+
615
+ def _calculate_lane_keeping(self) -> None:
616
+ """
617
+ Revised implementation of hydraMDP++'s lane keeping metric.
618
+ The trajectory is considered failing lane-keeping only if it deviates beyond
619
+ the lateral threshold continuously for at least certain seconds.
620
+
621
+ """
622
+ # Initialize lane-keeping scores to 1.0
623
+ lane_keeping_scores = np.ones(self._num_proposals, dtype=np.float64)
624
+ lateral_deviation_limit = self._config.lane_keeping_deviation_limit
625
+
626
+ interval_length = self.proposal_sampling.interval_length
627
+ continuous_steps_required = int(np.ceil(self._config.lane_keeping_horizon_window / interval_length))
628
+
629
+ centerline = self._centerline.linestring
630
+
631
+ for proposal_idx in range(self._num_proposals):
632
+ consecutive_exceeds = 0
633
+ for time_idx in range(self.proposal_sampling.num_poses + 1):
634
+ ego_position = Point(*self._ego_coords[proposal_idx, time_idx, BBCoordsIndex.CENTER])
635
+
636
+ is_in_intersection = self._drivable_area_map.is_in_layer(
637
+ ego_position, layer=SemanticMapLayer.INTERSECTION
638
+ )
639
+
640
+ if is_in_intersection:
641
+ continue
642
+
643
+ lateral_deviation = ego_position.distance(centerline)
644
+
645
+ if lateral_deviation > lateral_deviation_limit:
646
+ consecutive_exceeds += 1
647
+ else:
648
+ consecutive_exceeds = 0
649
+
650
+ if consecutive_exceeds >= continuous_steps_required:
651
+ lane_keeping_scores[proposal_idx] = 0.0
652
+ break
653
+
654
+ self._weighted_metrics[WeightedMetricIndex.LANE_KEEPING] = lane_keeping_scores
655
+
656
+ def _calculate_history_comfort(self) -> None:
657
+ """
658
+ Implementation of comfort metric, padded with past history states.
659
+ """
660
+
661
+ is_history_comfortable = np.ones(self._num_proposals, dtype=np.float64)
662
+
663
+ if self._human_past_trajectory is not None:
664
+
665
+ # interpolate human past trajectory
666
+ history_start_time_us = self._human_past_trajectory.start_time.time_us
667
+ history_end_time_us = self._human_past_trajectory.end_time.time_us
668
+ time_interval_us = int(0.1 * 1e6)
669
+
670
+ history_time_us = np.arange(
671
+ history_start_time_us,
672
+ history_end_time_us,
673
+ time_interval_us,
674
+ dtype=np.int64,
675
+ )
676
+ history_time_us = np.clip(history_time_us, history_start_time_us, history_end_time_us)
677
+
678
+ history_timepoints = [TimePoint(time_us) for time_us in history_time_us[:-1]]
679
+
680
+ history_state_array = ego_states_to_state_array(
681
+ self._human_past_trajectory.get_state_at_times(history_timepoints)
682
+ )
683
+
684
+ # Create state array padded with past human states
685
+ num_padded_poses = len(history_state_array) + self._states.shape[1]
686
+ padded_states_array = np.zeros(
687
+ (self._num_proposals, num_padded_poses, StateIndex.size()),
688
+ dtype=np.float64,
689
+ )
690
+
691
+ padded_states_array[:, : len(history_state_array)] = history_state_array
692
+ padded_states_array[:, len(history_state_array) :] = self._states
693
+
694
+ # create new timepoints with padding and compute comfort scores
695
+ time_point_s: npt.NDArray[np.float64] = (
696
+ np.arange(0, num_padded_poses).astype(np.float64) * self.proposal_sampling.interval_length
697
+ )
698
+ is_history_comfortable = ego_is_comfortable(padded_states_array, time_point_s).all(axis=-1)
699
+
700
+ self._weighted_metrics[WeightedMetricIndex.HISTORY_COMFORT] = is_history_comfortable
navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_scorer_utils.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+ from nuplan.common.actor_state.state_representation import StateSE2
4
+ from nuplan.common.actor_state.tracked_objects import TrackedObject
5
+ from nuplan.planning.metrics.utils.collision_utils import CollisionType
6
+ from nuplan.planning.simulation.observation.idm.utils import is_agent_behind, is_track_stopped
7
+ from shapely import LineString, Polygon
8
+
9
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import StateIndex
10
+
11
+
12
+ def get_collision_type(
13
+ state: npt.NDArray[np.float64],
14
+ ego_polygon: Polygon,
15
+ tracked_object: TrackedObject,
16
+ tracked_object_polygon: Polygon,
17
+ stopped_speed_threshold: float = 5e-02,
18
+ ) -> CollisionType:
19
+ """
20
+ Classify collision between ego and the track.
21
+ :param ego_state: Ego's state at the current timestamp.
22
+ :param tracked_object: Tracked object.
23
+ :param stopped_speed_threshold: Threshold for 0 speed due to noise.
24
+ :return Collision type.
25
+ """
26
+
27
+ ego_speed = np.hypot(
28
+ state[StateIndex.VELOCITY_X],
29
+ state[StateIndex.VELOCITY_Y],
30
+ )
31
+
32
+ is_ego_stopped = float(ego_speed) <= stopped_speed_threshold
33
+
34
+ center_point = tracked_object_polygon.centroid
35
+ tracked_object_center = StateSE2(center_point.x, center_point.y, tracked_object.box.center.heading)
36
+
37
+ ego_rear_axle_pose: StateSE2 = StateSE2(*state[StateIndex.STATE_SE2])
38
+
39
+ # Collisions at (close-to) zero ego speed
40
+ if is_ego_stopped:
41
+ collision_type = CollisionType.STOPPED_EGO_COLLISION
42
+
43
+ # Collisions at (close-to) zero track speed
44
+ elif is_track_stopped(tracked_object):
45
+ collision_type = CollisionType.STOPPED_TRACK_COLLISION
46
+
47
+ # Rear collision when both ego and track are not stopped
48
+ elif is_agent_behind(ego_rear_axle_pose, tracked_object_center):
49
+ collision_type = CollisionType.ACTIVE_REAR_COLLISION
50
+
51
+ # Front bumper collision when both ego and track are not stopped
52
+ elif LineString(
53
+ [
54
+ ego_polygon.exterior.coords[0],
55
+ ego_polygon.exterior.coords[3],
56
+ ]
57
+ ).intersects(tracked_object_polygon):
58
+ collision_type = CollisionType.ACTIVE_FRONT_COLLISION
59
+
60
+ # Lateral collision when both ego and track are not stopped
61
+ else:
62
+ collision_type = CollisionType.ACTIVE_LATERAL_COLLISION
63
+
64
+ return collision_type
navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/scene_aggregator.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional, Tuple
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
7
+
8
+ from navsim.planning.simulation.planner.pdm_planner.scoring.pdm_comfort_metrics import ego_is_two_frame_extended_comfort
9
+
10
+
11
+ @dataclass
12
+ class SceneAggregator:
13
+ now_frame: str
14
+ previous_frame: str
15
+ score_df: pd.DataFrame
16
+ proposal_sampling: TrajectorySampling
17
+ second_stage: Optional[List[Tuple[str, str]]] = None
18
+ sigma_squared: float = 0.1 # sigma² parameter for the Gaussian kernel
19
+
20
+ def calculate_pseudo_closed_loop_weights(self, first_stage_row, second_stage_scores) -> pd.Series:
21
+ """
22
+ Calculate pseudo-closed-loop weights using Gaussian kernel method
23
+ """
24
+ pd.options.mode.copy_on_write = True
25
+
26
+ second_stage_scores = second_stage_scores.copy()
27
+
28
+ def _calc_squared_distance(row):
29
+ x1, y1 = first_stage_row["endpoint_x"], first_stage_row["endpoint_y"]
30
+ x2, y2 = row["start_point_x"], row["start_point_y"]
31
+
32
+ return (x1 - x2) ** 2 + (y1 - y2) ** 2
33
+
34
+ second_stage_scores["squared_distance"] = second_stage_scores.apply(_calc_squared_distance, axis=1)
35
+
36
+ # Calculate Gaussian kernel weights: exp(-squared_distance/(2*sigma²))
37
+ exponent = -second_stage_scores["squared_distance"] / (2 * self.sigma_squared)
38
+ second_stage_scores["weight"] = np.exp(exponent)
39
+
40
+ weight_sum = second_stage_scores["weight"].sum()
41
+
42
+ if np.isclose(weight_sum, 0.0) or np.isnan(weight_sum):
43
+ second_stage_scores["weight"] = 1.0 / len(second_stage_scores)
44
+ else:
45
+ second_stage_scores["weight"] /= weight_sum
46
+
47
+ return second_stage_scores[["weight"]].reset_index()
48
+
49
+ def _compute_two_frame_comfort(self, current_token: str, previous_token: str) -> float:
50
+ try:
51
+ current_row = self.score_df.loc[current_token]
52
+ prev_row = self.score_df.loc[previous_token]
53
+ except KeyError as e:
54
+ raise ValueError(f"Missing token in score_df: {e}")
55
+
56
+ current_states = current_row.get("ego_simulated_states")
57
+ prev_states = prev_row.get("ego_simulated_states")
58
+
59
+ interval_length = self.proposal_sampling.interval_length
60
+ observation_interval = current_row["start_time"] - prev_row["start_time"]
61
+
62
+ assert 0 < observation_interval < 0.55, f"Invalid interval {observation_interval}"
63
+
64
+ overlap_start = round(observation_interval / interval_length)
65
+ current_states_overlap = current_states[:-overlap_start]
66
+ prev_states_overlap = prev_states[overlap_start:]
67
+
68
+ n_overlap = current_states_overlap.shape[0]
69
+ time_point_s = np.arange(n_overlap) * interval_length
70
+
71
+ two_frame_comfort = ego_is_two_frame_extended_comfort(
72
+ current_states_overlap[None, :],
73
+ prev_states_overlap[None, :],
74
+ time_point_s,
75
+ )[0].astype(np.float64)
76
+
77
+ return two_frame_comfort
78
+
79
+ def aggregate_scores(self, one_stage_only=False) -> pd.DataFrame:
80
+ updates = []
81
+
82
+ if one_stage_only:
83
+ main_comfort = self._compute_two_frame_comfort(self.now_frame, self.previous_frame)
84
+ updates.append({"token": self.now_frame, "two_frame_extended_comfort": main_comfort})
85
+
86
+ else:
87
+ # =====First stage=====
88
+ main_comfort = self._compute_two_frame_comfort(self.now_frame, self.previous_frame)
89
+ updates.append({"token": self.now_frame, "two_frame_extended_comfort": main_comfort, "weight": 1.0})
90
+ updates.append({"token": self.previous_frame, "two_frame_extended_comfort": main_comfort, "weight": 1.0})
91
+
92
+ # =====Second stage=====
93
+ # t = 0s and t = 4s
94
+ second_stage_now_tokens = [pair[0] for pair in self.second_stage]
95
+ second_stage_now_scores = self.score_df.loc[second_stage_now_tokens]
96
+
97
+ first_stage_now_row = self.score_df.loc[self.now_frame]
98
+ weights_now = self.calculate_pseudo_closed_loop_weights(first_stage_now_row, second_stage_now_scores)
99
+
100
+ # t = -0.5s and t = 3.5s
101
+ second_stage_prev_tokens = [pair[1] for pair in self.second_stage]
102
+ second_stage_prev_scores = self.score_df.loc[second_stage_prev_tokens]
103
+
104
+ first_stage_prev_row = self.score_df.loc[self.previous_frame]
105
+ weights_prev = self.calculate_pseudo_closed_loop_weights(first_stage_prev_row, second_stage_prev_scores)
106
+
107
+ weights = pd.concat([weights_now, weights_prev], ignore_index=True)
108
+
109
+ weight_map = dict(zip(weights["token"], weights["weight"]))
110
+
111
+ for (now_token, prev_token) in self.second_stage:
112
+
113
+ two_frame_comfort = self._compute_two_frame_comfort(now_token, prev_token)
114
+ weight_now = weight_map[now_token]
115
+ weight_prev = weight_map[prev_token]
116
+
117
+ updates.append(
118
+ {"token": now_token, "two_frame_extended_comfort": two_frame_comfort, "weight": weight_now}
119
+ )
120
+ updates.append(
121
+ {"token": prev_token, "two_frame_extended_comfort": two_frame_comfort, "weight": weight_prev}
122
+ )
123
+
124
+ return pd.DataFrame(updates)
navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/__init__.py ADDED
File without changes
navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/batch_kinematic_bicycle.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ from nuplan.common.actor_state.ego_state import EgoState
6
+ from nuplan.common.actor_state.state_representation import TimePoint
7
+ from nuplan.common.actor_state.vehicle_parameters import VehicleParameters, get_pacifica_parameters
8
+ from nuplan.common.geometry.compute import principal_value
9
+
10
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import DynamicStateIndex, StateIndex
11
+
12
+
13
+ def forward_integrate(
14
+ init: npt.NDArray[np.float64],
15
+ delta: npt.NDArray[np.float64],
16
+ sampling_time: TimePoint,
17
+ ) -> npt.NDArray[np.float64]:
18
+ """
19
+ Performs a simple euler integration.
20
+ :param init: Initial state
21
+ :param delta: The rate of change of the state.
22
+ :param sampling_time: The time duration to propagate for.
23
+ :return: The result of integration
24
+ """
25
+ return init + delta * sampling_time.time_s
26
+
27
+
28
+ class BatchKinematicBicycleModel:
29
+ """
30
+ A batch-wise operating class describing the kinematic motion model where the rear axle is the point of reference.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ vehicle: VehicleParameters = get_pacifica_parameters(),
36
+ max_steering_angle: float = np.pi / 3,
37
+ accel_time_constant: float = 0.2,
38
+ steering_angle_time_constant: float = 0.05,
39
+ ):
40
+ """
41
+ Construct BatchKinematicBicycleModel.
42
+ :param vehicle: Vehicle parameters.
43
+ :param max_steering_angle: [rad] Maximum absolute value steering angle allowed by model.
44
+ :param accel_time_constant: low pass filter time constant for acceleration in s
45
+ :param steering_angle_time_constant: low pass filter time constant for steering angle in s
46
+ """
47
+ self._vehicle = vehicle
48
+ self._max_steering_angle = max_steering_angle
49
+ self._accel_time_constant = accel_time_constant
50
+ self._steering_angle_time_constant = steering_angle_time_constant
51
+
52
+ def get_state_dot(self, states: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
53
+ """
54
+ Calculates the changing rate of state array representation.
55
+ :param states: array describing the state of the ego-vehicle
56
+ :return: change rate across several state values
57
+ """
58
+ state_dots = np.zeros(states.shape, dtype=np.float64)
59
+
60
+ longitudinal_speeds = states[:, StateIndex.VELOCITY_X]
61
+
62
+ state_dots[:, StateIndex.X] = longitudinal_speeds * np.cos(states[:, StateIndex.HEADING])
63
+ state_dots[:, StateIndex.Y] = longitudinal_speeds * np.sin(states[:, StateIndex.HEADING])
64
+ state_dots[:, StateIndex.HEADING] = (
65
+ longitudinal_speeds * np.tan(states[:, StateIndex.STEERING_ANGLE]) / self._vehicle.wheel_base
66
+ )
67
+
68
+ state_dots[:, StateIndex.VELOCITY_2D] = states[:, StateIndex.ACCELERATION_2D]
69
+ state_dots[:, StateIndex.ACCELERATION_2D] = 0.0
70
+
71
+ state_dots[:, StateIndex.STEERING_ANGLE] = states[:, StateIndex.STEERING_RATE]
72
+
73
+ return state_dots
74
+
75
+ def _update_commands(
76
+ self,
77
+ states: npt.NDArray[np.float64],
78
+ command_states: npt.NDArray[np.float64],
79
+ sampling_time: TimePoint,
80
+ ) -> EgoState:
81
+ """
82
+ This function applies some first order control delay/a low pass filter to acceleration/steering.
83
+
84
+ :param state: Ego state
85
+ :param ideal_dynamic_state: The desired dynamic state for propagation
86
+ :param sampling_time: The time duration to propagate for
87
+ :return: propagating_state including updated dynamic_state
88
+ """
89
+
90
+ propagating_state: npt.NDArray[np.float64] = copy.deepcopy(states)
91
+
92
+ dt_control = sampling_time.time_s
93
+
94
+ accel = states[:, StateIndex.ACCELERATION_X]
95
+ steering_angle = states[:, StateIndex.STEERING_ANGLE]
96
+
97
+ ideal_accel_x = command_states[:, DynamicStateIndex.ACCELERATION_X]
98
+ ideal_steering_angle = dt_control * command_states[:, DynamicStateIndex.STEERING_RATE] + steering_angle
99
+
100
+ updated_accel_x = dt_control / (dt_control + self._accel_time_constant) * (ideal_accel_x - accel) + accel
101
+ updated_steering_angle = (
102
+ dt_control / (dt_control + self._steering_angle_time_constant) * (ideal_steering_angle - steering_angle)
103
+ + steering_angle
104
+ )
105
+ updated_steering_rate = (updated_steering_angle - steering_angle) / dt_control
106
+
107
+ propagating_state[:, StateIndex.ACCELERATION_X] = updated_accel_x
108
+ propagating_state[:, StateIndex.ACCELERATION_Y] = 0.0
109
+ propagating_state[:, StateIndex.STEERING_RATE] = updated_steering_rate
110
+
111
+ return propagating_state
112
+
113
+ def propagate_state(
114
+ self,
115
+ states: npt.NDArray[np.float64],
116
+ command_states: npt.NDArray[np.float64],
117
+ sampling_time: TimePoint,
118
+ ) -> npt.NDArray[np.float64]:
119
+ """
120
+ Propagates ego state array forward with motion model.
121
+ :param states: state array representation of the ego-vehicle
122
+ :param command_states: command array representation of controller
123
+ :param sampling_time: time to propagate [s]
124
+ :return: updated tate array representation of the ego-vehicle
125
+ """
126
+
127
+ assert len(states) == len(command_states), "Batch size of states and command_states does not match!"
128
+
129
+ propagating_state = self._update_commands(states, command_states, sampling_time)
130
+ output_state = copy.deepcopy(states)
131
+
132
+ # Compute state derivatives
133
+ state_dot = self.get_state_dot(propagating_state)
134
+
135
+ output_state[:, StateIndex.X] = forward_integrate(
136
+ states[:, StateIndex.X], state_dot[:, StateIndex.X], sampling_time
137
+ )
138
+ output_state[:, StateIndex.Y] = forward_integrate(
139
+ states[:, StateIndex.Y], state_dot[:, StateIndex.Y], sampling_time
140
+ )
141
+
142
+ output_state[:, StateIndex.HEADING] = principal_value(
143
+ forward_integrate(
144
+ states[:, StateIndex.HEADING],
145
+ state_dot[:, StateIndex.HEADING],
146
+ sampling_time,
147
+ )
148
+ )
149
+
150
+ output_state[:, StateIndex.VELOCITY_X] = forward_integrate(
151
+ states[:, StateIndex.VELOCITY_X],
152
+ state_dot[:, StateIndex.VELOCITY_X],
153
+ sampling_time,
154
+ )
155
+
156
+ # Lateral velocity is always zero in kinematic bicycle model
157
+ output_state[:, StateIndex.VELOCITY_Y] = 0.0
158
+
159
+ # Integrate steering angle and clip to bounds
160
+ output_state[:, StateIndex.STEERING_ANGLE] = np.clip(
161
+ forward_integrate(
162
+ propagating_state[:, StateIndex.STEERING_ANGLE],
163
+ state_dot[:, StateIndex.STEERING_ANGLE],
164
+ sampling_time,
165
+ ),
166
+ -self._max_steering_angle,
167
+ self._max_steering_angle,
168
+ )
169
+
170
+ output_state[:, StateIndex.ANGULAR_VELOCITY] = (
171
+ output_state[:, StateIndex.VELOCITY_X]
172
+ * np.tan(output_state[:, StateIndex.STEERING_ANGLE])
173
+ / self._vehicle.wheel_base
174
+ )
175
+
176
+ output_state[:, StateIndex.ACCELERATION_2D] = state_dot[:, StateIndex.VELOCITY_2D]
177
+
178
+ output_state[:, StateIndex.ANGULAR_ACCELERATION] = (
179
+ output_state[:, StateIndex.ANGULAR_VELOCITY] - states[:, StateIndex.ANGULAR_VELOCITY]
180
+ ) / sampling_time.time_s
181
+
182
+ output_state[:, StateIndex.STEERING_RATE] = state_dot[:, StateIndex.STEERING_ANGLE]
183
+
184
+ return output_state
navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/batch_lqr.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import IntEnum
2
+ from typing import Optional, Tuple
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ from nuplan.common.actor_state.vehicle_parameters import VehicleParameters, get_pacifica_parameters
7
+ from nuplan.planning.simulation.simulation_time_controller.simulation_iteration import SimulationIteration
8
+
9
+ from navsim.planning.simulation.planner.pdm_planner.simulation.batch_lqr_utils import (
10
+ _generate_profile_from_initial_condition_and_derivatives,
11
+ get_velocity_curvature_profiles_with_derivatives_from_poses,
12
+ )
13
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import DynamicStateIndex, StateIndex
14
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_geometry_utils import normalize_angle
15
+
16
+
17
+ class LateralStateIndex(IntEnum):
18
+ """
19
+ Index mapping for the lateral dynamics state vector.
20
+ """
21
+
22
+ LATERAL_ERROR = 0 # [m] The lateral error with respect to the planner centerline at the vehicle's rear axle center.
23
+ HEADING_ERROR = 1 # [rad] The heading error "".
24
+ STEERING_ANGLE = 2 # [rad] The wheel angle relative to the longitudinal axis of the vehicle.
25
+
26
+
27
+ class BatchLQRTracker:
28
+ """
29
+ Implements an LQR tracker for a kinematic bicycle model.
30
+
31
+ Tracker operates on a batch of proposals. Implementation directly based on the nuplan-devkit
32
+ Link: https://github.com/motional/nuplan-devkit
33
+
34
+ We decouple into two subsystems, longitudinal and lateral, with small angle approximations for linearization.
35
+ We then solve two sequential LQR subproblems to find acceleration and steering rate inputs.
36
+
37
+ Longitudinal Subsystem:
38
+ States: [velocity]
39
+ Inputs: [acceleration]
40
+ Dynamics (continuous time):
41
+ velocity_dot = acceleration
42
+
43
+ Lateral Subsystem (After Linearization/Small Angle Approximation):
44
+ States: [lateral_error, heading_error, steering_angle]
45
+ Inputs: [steering_rate]
46
+ Parameters: [velocity, curvature]
47
+ Dynamics (continuous time):
48
+ lateral_error_dot = velocity * heading_error
49
+ heading_error_dot = velocity * (steering_angle / wheelbase_length - curvature)
50
+ steering_angle_dot = steering_rate
51
+
52
+ The continuous time dynamics are discretized using Euler integration and zero-order-hold on the input.
53
+ In case of a stopping reference, we use a simplified stopping P controller instead of LQR.
54
+
55
+ The final control inputs passed on to the motion model are:
56
+ - acceleration
57
+ - steering_rate
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ q_longitudinal: npt.NDArray[np.float64] = [10.0],
63
+ r_longitudinal: npt.NDArray[np.float64] = [1.0],
64
+ q_lateral: npt.NDArray[np.float64] = [1.0, 10.0, 0.0],
65
+ r_lateral: npt.NDArray[np.float64] = [1.0],
66
+ discretization_time: float = 0.1,
67
+ tracking_horizon: int = 10,
68
+ jerk_penalty: float = 1e-4,
69
+ curvature_rate_penalty: float = 1e-2,
70
+ stopping_proportional_gain: float = 0.5,
71
+ stopping_velocity: float = 0.2,
72
+ vehicle: VehicleParameters = get_pacifica_parameters(),
73
+ ):
74
+ """
75
+ Constructor for LQR controller
76
+ :param q_longitudinal: The weights for the Q matrix for the longitudinal subystem.
77
+ :param r_longitudinal: The weights for the R matrix for the longitudinal subystem.
78
+ :param q_lateral: The weights for the Q matrix for the lateral subystem.
79
+ :param r_lateral: The weights for the R matrix for the lateral subystem.
80
+ :param discretization_time: [s] The time interval used for discretizing the continuous time dynamics.
81
+ :param tracking_horizon: How many discrete time steps ahead to consider for the LQR objective.
82
+ :param stopping_proportional_gain: The proportional_gain term for the P controller when coming to a stop.
83
+ :param stopping_velocity: [m/s] The velocity below which we are deemed to be stopping and we don't use LQR.
84
+ :param vehicle: Vehicle parameters
85
+ """
86
+ # Longitudinal LQR Parameters
87
+ assert len(q_longitudinal) == 1, "q_longitudinal should have 1 element (velocity)."
88
+ assert len(r_longitudinal) == 1, "r_longitudinal should have 1 element (acceleration)."
89
+ self._q_longitudinal: float = q_longitudinal[0]
90
+ self._r_longitudinal: float = r_longitudinal[0]
91
+
92
+ # Lateral LQR Parameters
93
+ assert len(q_lateral) == 3, "q_lateral should have 3 elements (lateral_error, heading_error, steering_angle)."
94
+ assert len(r_lateral) == 1, "r_lateral should have 1 element (steering_rate)."
95
+ self._q_lateral: npt.NDArray[np.float64] = np.diag(q_lateral)
96
+ self._r_lateral: npt.NDArray[np.float64] = np.diag(r_lateral)
97
+
98
+ # Common LQR Parameters
99
+ # Note we want a horizon > 1 so that steering rate actually can impact lateral/heading error in discrete time.
100
+ assert discretization_time > 0.0, "The discretization_time should be positive."
101
+ assert (
102
+ tracking_horizon > 1
103
+ ), "We expect the horizon to be greater than 1 - else steering_rate has no impact with Euler integration."
104
+ self._discretization_time = discretization_time
105
+ self._tracking_horizon = tracking_horizon
106
+ self._wheel_base = vehicle.wheel_base
107
+
108
+ # Velocity/Curvature Estimation Parameters
109
+ assert jerk_penalty > 0.0, "The jerk penalty must be positive."
110
+ assert curvature_rate_penalty > 0.0, "The curvature rate penalty must be positive."
111
+ self._jerk_penalty = jerk_penalty
112
+ self._curvature_rate_penalty = curvature_rate_penalty
113
+
114
+ # Stopping Controller Parameters
115
+ assert stopping_proportional_gain > 0, "stopping_proportional_gain has to be greater than 0."
116
+ assert stopping_velocity > 0, "stopping_velocity has to be greater than 0."
117
+ self._stopping_proportional_gain = stopping_proportional_gain
118
+ self._stopping_velocity = stopping_velocity
119
+
120
+ # lazy loaded
121
+ self._proposal_states: Optional[npt.NDArray[np.float64]] = None
122
+ self._initialized: bool = False
123
+
124
+ def update(self, proposal_states: npt.NDArray[np.float64]) -> None:
125
+ """
126
+ Loads proposal state array and resets velocity, and curvature profile.
127
+ :param proposal_states: array representation of proposals.
128
+ """
129
+ self._proposal_states: npt.NDArray[np.float64] = proposal_states
130
+ self._velocity_profile, self._curvature_profile = None, None
131
+ self._initialized = True
132
+
133
+ def track_trajectory(
134
+ self,
135
+ current_iteration: SimulationIteration,
136
+ next_iteration: SimulationIteration,
137
+ initial_states: npt.NDArray[np.float64],
138
+ ) -> npt.NDArray[np.float64]:
139
+ """
140
+ Calculates the command values given the proposals to track.
141
+ :param current_iteration: current simulation iteration.
142
+ :param next_iteration: desired next simulation iteration.
143
+ :param initial_states: array representation of current ego states.
144
+ :return: command values for motion model.
145
+ """
146
+ assert self._initialized, "BatchLQRTracker: Run update first to load proposal states!"
147
+
148
+ batch_size = len(initial_states)
149
+ (initial_velocity, initial_lateral_state_vector,) = self._compute_initial_velocity_and_lateral_state(
150
+ current_iteration, initial_states
151
+ ) # (batch), (batch, 3)
152
+
153
+ (reference_velocities, curvature_profiles,) = self._compute_reference_velocity_and_curvature_profile(
154
+ current_iteration
155
+ ) # (batch), (batch, 10)
156
+
157
+ # create output arrays
158
+ accel_cmds = np.zeros(batch_size, dtype=np.float64)
159
+ steering_rate_cmds = np.zeros(batch_size, dtype=np.float64)
160
+
161
+ # 1. Stopping Controller
162
+ should_stop_mask = np.logical_and(
163
+ reference_velocities <= self._stopping_velocity,
164
+ initial_velocity <= self._stopping_velocity,
165
+ )
166
+ stopping_accel_cmd, stopping_steering_rate_cmd = self._stopping_controller(
167
+ initial_velocity[should_stop_mask], reference_velocities[should_stop_mask]
168
+ )
169
+ accel_cmds[should_stop_mask] = stopping_accel_cmd
170
+ steering_rate_cmds[should_stop_mask] = stopping_steering_rate_cmd
171
+
172
+ # 2. Regular Controller
173
+ accel_cmds[~should_stop_mask] = self._longitudinal_lqr_controller(
174
+ initial_velocity[~should_stop_mask], reference_velocities[~should_stop_mask]
175
+ )
176
+
177
+ velocity_profiles = _generate_profile_from_initial_condition_and_derivatives(
178
+ initial_condition=initial_velocity[~should_stop_mask],
179
+ derivatives=np.repeat(accel_cmds[~should_stop_mask, None], self._tracking_horizon, axis=-1),
180
+ discretization_time=self._discretization_time,
181
+ )[:, : self._tracking_horizon]
182
+
183
+ steering_rate_cmds[~should_stop_mask] = self._lateral_lqr_controller(
184
+ initial_lateral_state_vector[~should_stop_mask],
185
+ velocity_profiles,
186
+ curvature_profiles[~should_stop_mask],
187
+ )
188
+
189
+ command_states = np.zeros((batch_size, len(DynamicStateIndex)), dtype=np.float64)
190
+ command_states[:, DynamicStateIndex.ACCELERATION_X] = accel_cmds
191
+ command_states[:, DynamicStateIndex.STEERING_RATE] = steering_rate_cmds
192
+
193
+ return command_states
194
+
195
+ def _compute_initial_velocity_and_lateral_state(
196
+ self,
197
+ current_iteration: SimulationIteration,
198
+ initial_values: npt.NDArray[np.float64],
199
+ ) -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
200
+ """
201
+ This method projects the initial tracking error into vehicle/Frenet frame. It also extracts initial velocity.
202
+ :param current_iteration: Used to get the current time.
203
+ :param initial_state: The current state for ego.
204
+ :param trajectory: The reference trajectory we are tracking.
205
+ :return: Initial velocity [m/s] and initial lateral state.
206
+ """
207
+ # Get initial trajectory state.
208
+ initial_trajectory_values = self._proposal_states[:, current_iteration.index]
209
+
210
+ # Determine initial error state.
211
+ x_errors = initial_values[:, StateIndex.X] - initial_trajectory_values[:, StateIndex.X]
212
+ y_errors = initial_values[:, StateIndex.Y] - initial_trajectory_values[:, StateIndex.Y]
213
+ heading_references = initial_trajectory_values[:, StateIndex.HEADING]
214
+
215
+ lateral_errors = -x_errors * np.sin(heading_references) + y_errors * np.cos(heading_references)
216
+ heading_errors = normalize_angle(initial_values[:, StateIndex.HEADING] - heading_references)
217
+
218
+ # Return initial velocity and lateral state vector.
219
+ initial_velocities = initial_values[:, StateIndex.VELOCITY_X]
220
+
221
+ initial_lateral_state_vector = np.stack(
222
+ [
223
+ lateral_errors,
224
+ heading_errors,
225
+ initial_values[:, StateIndex.STEERING_ANGLE],
226
+ ],
227
+ axis=-1,
228
+ )
229
+
230
+ return initial_velocities, initial_lateral_state_vector
231
+
232
+ def _compute_reference_velocity_and_curvature_profile(
233
+ self,
234
+ current_iteration: SimulationIteration,
235
+ ) -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
236
+ """
237
+ This method computes reference velocity and curvature profile based on the reference trajectory.
238
+ We use a lookahead time equal to self._tracking_horizon * self._discretization_time.
239
+ :param current_iteration: Used to get the current time.
240
+ :param trajectory: The reference trajectory we are tracking.
241
+ :return: The reference velocity [m/s] and curvature profile [rad] to track.
242
+ """
243
+
244
+ poses = self._proposal_states[..., StateIndex.STATE_SE2]
245
+
246
+ if self._velocity_profile is None or self._curvature_profile is None:
247
+ (
248
+ self._velocity_profile,
249
+ acceleration_profile,
250
+ self._curvature_profile,
251
+ curvature_rate_profile,
252
+ ) = get_velocity_curvature_profiles_with_derivatives_from_poses(
253
+ discretization_time=self._discretization_time,
254
+ poses=poses,
255
+ jerk_penalty=self._jerk_penalty,
256
+ curvature_rate_penalty=self._curvature_rate_penalty,
257
+ )
258
+
259
+ batch_size, num_poses = self._velocity_profile.shape
260
+ reference_idx = min(current_iteration.index + self._tracking_horizon, num_poses - 1)
261
+ reference_velocities = self._velocity_profile[:, reference_idx]
262
+
263
+ reference_curvature_profiles = np.zeros((batch_size, self._tracking_horizon), dtype=np.float64)
264
+
265
+ reference_length = reference_idx - current_iteration.index
266
+ reference_curvature_profiles[:, 0:reference_length] = self._curvature_profile[
267
+ :, current_iteration.index : reference_idx
268
+ ]
269
+
270
+ if reference_length < self._tracking_horizon:
271
+ reference_curvature_profiles[:, reference_length:] = self._curvature_profile[:, reference_idx, None]
272
+
273
+ return reference_velocities, reference_curvature_profiles
274
+
275
+ def _stopping_controller(
276
+ self,
277
+ initial_velocities: npt.NDArray[np.float64],
278
+ reference_velocities: npt.NDArray[np.float64],
279
+ ) -> Tuple[float, float]:
280
+ """
281
+ Apply proportional controller when at near-stop conditions.
282
+ :param initial_velocity: [m/s] The current velocity of ego.
283
+ :param reference_velocity: [m/s] The reference velocity to track.
284
+ :return: Acceleration [m/s^2] and zero steering_rate [rad/s] command.
285
+ """
286
+ accel = -self._stopping_proportional_gain * (initial_velocities - reference_velocities)
287
+ return accel, 0.0
288
+
289
+ def _longitudinal_lqr_controller(
290
+ self,
291
+ initial_velocities: npt.NDArray[np.float64],
292
+ reference_velocities: npt.NDArray[np.float64],
293
+ ) -> npt.NDArray[np.float64]:
294
+ """
295
+ This longitudinal controller determines an acceleration input to minimize velocity error at a lookahead time.
296
+ :param initial_velocity: [m/s] The current velocity of ego.
297
+ :param reference_velocity: [m/s] The reference_velocity to track at a lookahead time.
298
+ :return: Acceleration [m/s^2] command based on LQR.
299
+ """
300
+ # We assume that we hold the acceleration constant for the entire tracking horizon.
301
+ # Given this, we can show the following where N = self._tracking_horizon and dt = self._discretization_time:
302
+ # velocity_N = velocity_0 + (N * dt) * acceleration
303
+
304
+ batch_size = len(initial_velocities)
305
+
306
+ A: npt.NDArray[np.float64] = np.ones(batch_size, dtype=np.float64)
307
+
308
+ B: npt.NDArray[np.float64] = np.zeros(batch_size, dtype=np.float64)
309
+ B.fill(self._tracking_horizon * self._discretization_time)
310
+
311
+ g: npt.NDArray[np.float64] = np.zeros(batch_size, dtype=np.float64)
312
+
313
+ accel_cmds = self._solve_one_step_longitudinal_lqr(
314
+ initial_state=initial_velocities,
315
+ reference_state=reference_velocities,
316
+ A=A,
317
+ B=B,
318
+ g=g,
319
+ )
320
+
321
+ return accel_cmds
322
+
323
+ def _lateral_lqr_controller(
324
+ self,
325
+ initial_lateral_state_vector: npt.NDArray[np.float64],
326
+ velocity_profile: npt.NDArray[np.float64],
327
+ curvature_profile: npt.NDArray[np.float64],
328
+ ) -> float:
329
+ """
330
+ This lateral controller determines a steering_rate input to minimize lateral errors at a lookahead time.
331
+ It requires a velocity sequence as a parameter to ensure linear time-varying lateral dynamics.
332
+ :param initial_lateral_state_vector: The current lateral state of ego.
333
+ :param velocity_profile: [m/s] The velocity over the entire self._tracking_horizon-step lookahead.
334
+ :param curvature_profile: [rad] The curvature over the entire self._tracking_horizon-step lookahead..
335
+ :return: Steering rate [rad/s] command based on LQR.
336
+ """
337
+ assert velocity_profile.shape[-1] == self._tracking_horizon, (
338
+ f"The linearization velocity sequence should have length {self._tracking_horizon} "
339
+ f"but is {len(velocity_profile)}."
340
+ )
341
+ assert curvature_profile.shape[-1] == self._tracking_horizon, (
342
+ f"The linearization curvature sequence should have length {self._tracking_horizon} "
343
+ f"but is {len(curvature_profile)}."
344
+ )
345
+
346
+ batch_dim = velocity_profile.shape[0]
347
+
348
+ # Set up the lateral LQR problem using the constituent linear time-varying (affine) system dynamics.
349
+ # Ultimately, we'll end up with the following problem structure where N = self._tracking_horizon:
350
+ # lateral_error_N = A @ lateral_error_0 + B @ steering_rate + g
351
+ n_lateral_states = len(LateralStateIndex)
352
+
353
+ I: npt.NDArray[np.float64] = np.eye(n_lateral_states, dtype=np.float64)
354
+
355
+ in_matrix: npt.NDArray[np.float64] = np.zeros((n_lateral_states, 1), np.float64) # no batch dim
356
+ in_matrix[LateralStateIndex.STEERING_ANGLE] = self._discretization_time
357
+
358
+ states_matrix_at_step: npt.NDArray[np.float64] = np.tile(
359
+ I[None, None, ...], [self._tracking_horizon, batch_dim, 1, 1]
360
+ ) # (horizon, batch, 3, 3)
361
+
362
+ states_matrix_at_step[:, :, LateralStateIndex.LATERAL_ERROR, LateralStateIndex.HEADING_ERROR] = (
363
+ velocity_profile.T * self._discretization_time
364
+ )
365
+
366
+ states_matrix_at_step[:, :, LateralStateIndex.HEADING_ERROR, LateralStateIndex.STEERING_ANGLE] = (
367
+ velocity_profile.T * self._discretization_time / self._wheel_base
368
+ )
369
+
370
+ affine_terms: npt.NDArray[np.float64] = np.zeros(
371
+ (self._tracking_horizon, batch_dim, n_lateral_states), dtype=np.float64
372
+ )
373
+
374
+ affine_terms[:, :, LateralStateIndex.HEADING_ERROR] = (
375
+ -velocity_profile.T * curvature_profile.T * self._discretization_time
376
+ )
377
+
378
+ A: npt.NDArray[np.float64] = np.tile(I[None, ...], [batch_dim, 1, 1]) # (batch, 3, 3)
379
+ B: npt.NDArray[np.float64] = np.zeros((batch_dim, n_lateral_states, 1), dtype=np.float64) # (batch, 3, 1)
380
+ g: npt.NDArray[np.float64] = np.zeros((batch_dim, n_lateral_states), dtype=np.float64) # (batch, 3)
381
+
382
+ for index_step, (state_matrix_at_step, affine_term) in enumerate(zip(states_matrix_at_step, affine_terms)):
383
+ # state_matrix_at_step (batch, 3, 3)
384
+ # affine_term (batch, 3)
385
+ A = np.einsum("bij, bjk -> bik", state_matrix_at_step, A) # (batch, 3, 3)
386
+ B = np.einsum("bij, bjk -> bik", state_matrix_at_step, B) + in_matrix # (batch, 3, 1)
387
+ g = np.einsum("bij, bj -> bi", state_matrix_at_step, g) + affine_term # (batch, 3)
388
+
389
+ steering_rate_cmd = self._solve_one_step_lateral_lqr(
390
+ initial_state=initial_lateral_state_vector,
391
+ A=A,
392
+ B=B,
393
+ g=g,
394
+ )
395
+
396
+ return np.squeeze(steering_rate_cmd, axis=-1)
397
+
398
+ def _solve_one_step_longitudinal_lqr(
399
+ self,
400
+ initial_state: npt.NDArray[np.float64],
401
+ reference_state: npt.NDArray[np.float64],
402
+ A: npt.NDArray[np.float64],
403
+ B: npt.NDArray[np.float64],
404
+ g: npt.NDArray[np.float64],
405
+ ) -> npt.NDArray[np.float64]:
406
+ """
407
+ This function uses LQR to find an optimal input to minimize tracking error in one step of dynamics.
408
+ The dynamics are next_state = A @ initial_state + B @ input + g and our target is the reference_state.
409
+ :param initial_state: The current state.
410
+ :param reference_state: The desired state in 1 step (according to A,B,g dynamics).
411
+ :param A: The state dynamics matrix.
412
+ :param B: The input dynamics matrix.
413
+ :param g: The offset/affine dynamics term.
414
+ :return: LQR optimal input for the 1-step longitudinal problem.
415
+ """
416
+ state_error_zero_input = A * initial_state + g - reference_state
417
+ inverse = -1 / (B * self._q_longitudinal * B + self._r_longitudinal)
418
+ lqr_input = inverse * B * self._q_longitudinal * state_error_zero_input
419
+
420
+ return lqr_input
421
+
422
+ def _solve_one_step_lateral_lqr(
423
+ self,
424
+ initial_state: npt.NDArray[np.float64],
425
+ A: npt.NDArray[np.float64],
426
+ B: npt.NDArray[np.float64],
427
+ g: npt.NDArray[np.float64],
428
+ ) -> npt.NDArray[np.float64]:
429
+ """
430
+ This function uses LQR to find an optimal input to minimize tracking error in one step of dynamics.
431
+ The dynamics are next_state = A @ initial_state + B @ input + g and our target is the reference_state.
432
+ :param initial_state: The current state.
433
+ :param A: The state dynamics matrix.
434
+ :param B: The input dynamics matrix.
435
+ :param g: The offset/affine dynamics term.
436
+ :return: LQR optimal input for the 1-step lateral problem.
437
+ """
438
+
439
+ Q, R = self._q_lateral, self._r_lateral
440
+ angle_diff_indices = [
441
+ LateralStateIndex.HEADING_ERROR.value,
442
+ LateralStateIndex.STEERING_ANGLE.value,
443
+ ]
444
+ BT = B.transpose(0, 2, 1)
445
+
446
+ state_error_zero_input = np.einsum("bij, bj -> bi", A, initial_state) + g
447
+
448
+ angle = state_error_zero_input[..., angle_diff_indices]
449
+ state_error_zero_input[..., angle_diff_indices] = np.arctan2(np.sin(angle), np.cos(angle))
450
+
451
+ BT_x_Q = np.einsum("bij, jk -> bik", BT, Q)
452
+ Inv = -1 / (np.einsum("bij, bji -> bi", BT_x_Q, B) + R)
453
+ Tail = np.einsum("bij, bj -> bi", BT_x_Q, state_error_zero_input)
454
+
455
+ lqr_input = Inv * Tail
456
+
457
+ return lqr_input
navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/batch_lqr_utils.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+
6
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_geometry_utils import normalize_angle
7
+
8
+ # Util functions for BatchLQRTracker
9
+ # Code re-written based on nuPlan's implementation:
10
+ # https://github.com/motional/nuplan-devkit
11
+
12
+ # Default regularization weight for initial curvature fit. Users shouldn't really need to modify this,
13
+ # we just want it positive and small for improved conditioning of the associated least squares problem.
14
+ INITIAL_CURVATURE_PENALTY = 1e-10
15
+
16
+ # helper function to apply matrix multiplication over a batch-dim
17
+ batch_matmul = lambda a, b: np.einsum("bij, bjk -> bik", a, b)
18
+
19
+
20
+ def _generate_profile_from_initial_condition_and_derivatives(
21
+ initial_condition: npt.NDArray[np.float64],
22
+ derivatives: npt.NDArray[np.float64],
23
+ discretization_time: float,
24
+ ) -> npt.NDArray[np.float64]:
25
+ """
26
+ Returns the corresponding profile (i.e. trajectory) given an initial condition and derivatives at
27
+ multiple timesteps by integration.
28
+ :param initial_condition: The value of the variable at the initial timestep.
29
+ :param derivatives: The trajectory of time derivatives of the variable at timesteps 0,..., N-1.
30
+ :param discretization_time: [s] Time discretization used for integration.
31
+ :return: The trajectory of the variable at timesteps 0,..., N.
32
+ """
33
+ assert discretization_time > 0.0, "Discretization time must be positive."
34
+ cumsum = np.cumsum(derivatives * discretization_time, axis=-1)
35
+ profile = initial_condition[..., None] + np.pad(cumsum, [(0, 0), (1, 0)], mode="constant")
36
+ return profile
37
+
38
+
39
+ def _get_xy_heading_displacements_from_poses(
40
+ poses: npt.NDArray[np.float64],
41
+ ) -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
42
+ """
43
+ Returns position and heading displacements given a pose trajectory.
44
+ :param poses: <np.ndarray: num_poses, 3> A trajectory of poses (x, y, heading).
45
+ :return: Tuple of xy displacements with shape (num_poses-1, 2) and heading displacements with shape (num_poses-1,).
46
+ """
47
+ assert len(poses.shape) == 3, "Expect a 2D matrix representing a trajectory of poses."
48
+ assert poses.shape[1] > 1, "Cannot get displacements given an empty or single element pose trajectory."
49
+ assert poses.shape[2] == 3, "Expect pose to have three elements (x, y, heading)."
50
+
51
+ # Compute displacements that are used to complete the kinematic state and input.
52
+ pose_differences = np.diff(poses, axis=1) # (b, num_poses-1, 3)
53
+ xy_displacements = pose_differences[..., :2]
54
+ heading_displacements = normalize_angle(pose_differences[..., 2])
55
+
56
+ return xy_displacements, heading_displacements
57
+
58
+
59
+ def _make_banded_difference_matrix(number_rows: int) -> npt.NDArray[np.float64]:
60
+ """
61
+ Returns a banded difference matrix with specified number_rows.
62
+ When applied to a vector [x_1, ..., x_N], it returns [x_2 - x_1, ..., x_N - x_{N-1}].
63
+ :param number_rows: The row dimension of the banded difference matrix (e.g. N-1 in the example above).
64
+ :return: A banded difference matrix with shape (number_rows, number_rows+1).
65
+ """
66
+ banded_matrix = np.zeros((number_rows, number_rows + 1), dtype=np.float64)
67
+ eye = np.eye(number_rows, dtype=np.float64)
68
+ banded_matrix[:, 1:] = eye
69
+ banded_matrix[:, :-1] = -eye
70
+ return banded_matrix
71
+
72
+
73
+ def _fit_initial_velocity_and_acceleration_profile(
74
+ xy_displacements: npt.NDArray[np.float64],
75
+ heading_profile: npt.NDArray[np.float64],
76
+ discretization_time: float,
77
+ jerk_penalty: float,
78
+ ) -> Tuple[float, npt.NDArray[np.float64]]:
79
+ """
80
+ Estimates initial velocity (v_0) and acceleration ({a_0, ...}) using least squares with jerk penalty regularization.
81
+ :param xy_displacements: [m] Deviations in x and y occurring between M+1 poses, a M by 2 matrix.
82
+ :param heading_profile: [rad] Headings associated to the starting timestamp for xy_displacements, a M-length vector.
83
+ :param discretization_time: [s] Time discretization used for integration.
84
+ :param jerk_penalty: A regularization parameter used to penalize acceleration differences. Should be positive.
85
+ :return: Least squares solution for initial velocity (v_0) and acceleration profile ({a_0, ..., a_M-1})
86
+ for M displacement values.
87
+ """
88
+ assert discretization_time > 0.0, "Discretization time must be positive."
89
+ assert jerk_penalty > 0, "Should have a positive jerk_penalty."
90
+
91
+ assert len(xy_displacements.shape) == 3, "Expect xy_displacements to be a matrix."
92
+ assert xy_displacements.shape[2] == 2, "Expect xy_displacements to have 2 columns."
93
+
94
+ num_displacements = xy_displacements.shape[1] # aka M in the docstring
95
+ assert heading_profile.shape[0] == xy_displacements.shape[0]
96
+
97
+ batch_size = heading_profile.shape[0]
98
+ # Core problem: minimize_x ||y-Ax||_2
99
+ y = xy_displacements.reshape(batch_size, -1) # Flatten to a vector, [delta x_0, delta y_0, ...]
100
+
101
+ headings = np.array(heading_profile, dtype=np.float64)
102
+ A_column = np.zeros(y.shape, dtype=np.float64)
103
+ A_column[:, 0::2] = np.cos(headings)
104
+ A_column[:, 1::2] = np.sin(headings)
105
+
106
+ A = np.repeat(A_column[..., None] * discretization_time**2, num_displacements, axis=2)
107
+ A[..., 0] = A_column * discretization_time
108
+
109
+ upper_triangle_mask = np.triu(np.ones((num_displacements, num_displacements), dtype=bool), k=1)
110
+ upper_triangle_mask = np.repeat(upper_triangle_mask, 2, axis=0)
111
+ A[:, upper_triangle_mask] = 0.0
112
+
113
+ # Regularization using jerk penalty, i.e. difference of acceleration values.
114
+ # If there are M displacements, then we have M - 1 acceleration values.
115
+ # That means we have M - 2 jerk values, thus we make a banded difference matrix of that size.
116
+ banded_matrix = _make_banded_difference_matrix(num_displacements - 2)
117
+ R: npt.NDArray[np.float64] = np.block([np.zeros((len(banded_matrix), 1)), banded_matrix])
118
+ R = np.repeat(R[None, ...], batch_size, axis=0)
119
+
120
+ A_T, R_T = np.transpose(A, (0, 2, 1)), np.transpose(R, (0, 2, 1))
121
+
122
+ # Compute regularized least squares solution.
123
+ intermediate_solution = batch_matmul(
124
+ np.linalg.pinv(batch_matmul(A_T, A) + jerk_penalty * batch_matmul(R_T, R)), A_T
125
+ )
126
+ x = np.einsum("bij, bj -> bi", intermediate_solution, y)
127
+
128
+ # Extract profile from solution.
129
+ initial_velocity = x[:, 0]
130
+ acceleration_profile = x[:, 1:]
131
+
132
+ return initial_velocity, acceleration_profile
133
+
134
+
135
+ def _fit_initial_curvature_and_curvature_rate_profile(
136
+ heading_displacements: npt.NDArray[np.float64],
137
+ velocity_profile: npt.NDArray[np.float64],
138
+ discretization_time: float,
139
+ curvature_rate_penalty: float,
140
+ initial_curvature_penalty: float = INITIAL_CURVATURE_PENALTY,
141
+ ) -> Tuple[float, npt.NDArray[np.float64]]:
142
+ """
143
+ Estimates initial curvature (curvature_0) and curvature rate ({curvature_rate_0, ...})
144
+ using least squares with curvature rate regularization.
145
+ :param heading_displacements: [rad] Angular deviations in heading occurring between timesteps.
146
+ :param velocity_profile: [m/s] Estimated or actual velocities at the timesteps matching displacements.
147
+ :param discretization_time: [s] Time discretization used for integration.
148
+ :param curvature_rate_penalty: A regularization parameter used to penalize curvature_rate. Should be positive.
149
+ :param initial_curvature_penalty: A regularization parameter to handle zero initial speed. Should be positive and small.
150
+ :return: Least squares solution for initial curvature (curvature_0) and curvature rate profile
151
+ (curvature_rate_0, ..., curvature_rate_{M-1}) for M heading displacement values.
152
+ """
153
+ assert discretization_time > 0.0, "Discretization time must be positive."
154
+ assert curvature_rate_penalty > 0.0, "Should have a positive curvature_rate_penalty."
155
+ assert initial_curvature_penalty > 0.0, "Should have a positive initial_curvature_penalty."
156
+
157
+ # Core problem: minimize_x ||y-Ax||_2
158
+ y = heading_displacements
159
+ batch_dim, dim = y.shape
160
+
161
+ A: npt.NDArray[np.float64] = np.repeat(
162
+ np.tri(dim, dtype=np.float64)[None, ...], batch_dim, axis=0
163
+ ) # lower triangular matrix
164
+
165
+ A[:, :, 0] = velocity_profile * discretization_time
166
+
167
+ velocity = velocity_profile * discretization_time**2
168
+ A[:, 1:, 1:] *= velocity[:, None, 1:].transpose(0, 2, 1)
169
+
170
+ # Regularization on curvature rate. We add a small but nonzero weight on initial curvature too.
171
+ # This is since the corresponding row of the A matrix might be zero if initial speed is 0, leading to singularity.
172
+ # We guarantee that Q is positive definite such that the minimizer of the least squares problem is unique.
173
+ Q: npt.NDArray[np.float64] = curvature_rate_penalty * np.eye(dim)
174
+ Q[0, 0] = initial_curvature_penalty
175
+
176
+ # Compute regularized least squares solution.
177
+ A_T = A.transpose(0, 2, 1)
178
+
179
+ intermediate = batch_matmul(np.linalg.pinv(batch_matmul(A_T, A) + Q), A_T)
180
+ x = np.einsum("bij,bj->bi", intermediate, y)
181
+
182
+ # Extract profile from solution.
183
+ initial_curvature = x[:, 0]
184
+ curvature_rate_profile = x[:, 1:]
185
+
186
+ return initial_curvature, curvature_rate_profile
187
+
188
+
189
+ def get_velocity_curvature_profiles_with_derivatives_from_poses(
190
+ discretization_time: float,
191
+ poses: npt.NDArray[np.float64],
192
+ jerk_penalty: float,
193
+ curvature_rate_penalty: float,
194
+ ) -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.float64],]:
195
+ """
196
+ Main function for joint estimation of velocity, acceleration, curvature, and curvature rate given N poses
197
+ sampled at discretization_time. This is done by solving two least squares problems with the given penalty weights.
198
+ :param discretization_time: [s] Time discretization used for integration.
199
+ :param poses: <np.ndarray: num_poses, 3> A trajectory of N poses (x, y, heading).
200
+ :param jerk_penalty: A regularization parameter used to penalize acceleration differences. Should be positive.
201
+ :param curvature_rate_penalty: A regularization parameter used to penalize curvature_rate. Should be positive.
202
+ :return: Profiles for velocity (N-1), acceleration (N-2), curvature (N-1), and curvature rate (N-2).
203
+ """
204
+ xy_displacements, heading_displacements = _get_xy_heading_displacements_from_poses(poses)
205
+
206
+ (initial_velocity, acceleration_profile,) = _fit_initial_velocity_and_acceleration_profile(
207
+ xy_displacements=xy_displacements,
208
+ heading_profile=poses[:, :-1, 2],
209
+ discretization_time=discretization_time,
210
+ jerk_penalty=jerk_penalty,
211
+ )
212
+
213
+ velocity_profile = _generate_profile_from_initial_condition_and_derivatives(
214
+ initial_condition=initial_velocity,
215
+ derivatives=acceleration_profile,
216
+ discretization_time=discretization_time,
217
+ )
218
+
219
+ # Compute initial curvature + curvature rate least squares solution and extract results. It relies on velocity fit.
220
+ (initial_curvature, curvature_rate_profile,) = _fit_initial_curvature_and_curvature_rate_profile(
221
+ heading_displacements=heading_displacements,
222
+ velocity_profile=velocity_profile,
223
+ discretization_time=discretization_time,
224
+ curvature_rate_penalty=curvature_rate_penalty,
225
+ )
226
+
227
+ curvature_profile = _generate_profile_from_initial_condition_and_derivatives(
228
+ initial_condition=initial_curvature,
229
+ derivatives=curvature_rate_profile,
230
+ discretization_time=discretization_time,
231
+ )
232
+
233
+ return (
234
+ velocity_profile,
235
+ acceleration_profile,
236
+ curvature_profile,
237
+ curvature_rate_profile,
238
+ )
navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/pdm_simulator.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+ from nuplan.common.actor_state.ego_state import EgoState
4
+ from nuplan.common.actor_state.state_representation import TimeDuration, TimePoint
5
+ from nuplan.planning.simulation.simulation_time_controller.simulation_iteration import SimulationIteration
6
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
7
+
8
+ from navsim.planning.simulation.planner.pdm_planner.simulation.batch_kinematic_bicycle import BatchKinematicBicycleModel
9
+ from navsim.planning.simulation.planner.pdm_planner.simulation.batch_lqr import BatchLQRTracker
10
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_array_representation import ego_state_to_state_array
11
+
12
+
13
+ class PDMSimulator:
14
+ """
15
+ Re-implementation of nuPlan's simulation pipeline. Enables batch-wise simulation.
16
+ """
17
+
18
+ def __init__(self, proposal_sampling: TrajectorySampling):
19
+ """
20
+ Constructor of PDMSimulator.
21
+ :param proposal_sampling: Sampling parameters for proposals
22
+ """
23
+
24
+ # time parameters
25
+ self.proposal_sampling = proposal_sampling
26
+
27
+ # simulation objects
28
+ self._motion_model = BatchKinematicBicycleModel()
29
+ self._tracker = BatchLQRTracker()
30
+
31
+ def simulate_proposals(
32
+ self, states: npt.NDArray[np.float64], initial_ego_state: EgoState
33
+ ) -> npt.NDArray[np.float64]:
34
+ """
35
+ Simulate all proposals over batch-dim
36
+ :param initial_ego_state: ego-vehicle state at current iteration
37
+ :param states: proposal states as array
38
+ :return: simulated proposal states as array
39
+ """
40
+
41
+ # TODO: find cleaner way to load parameters
42
+ # set parameters of motion model and tracker
43
+ self._motion_model._vehicle = initial_ego_state.car_footprint.vehicle_parameters
44
+ self._tracker._discretization_time = self.proposal_sampling.interval_length
45
+
46
+ proposal_states = states[:, : self.proposal_sampling.num_poses + 1]
47
+ self._tracker.update(proposal_states)
48
+
49
+ # state array representation for simulated vehicle states
50
+ simulated_states = np.zeros(proposal_states.shape, dtype=np.float64)
51
+ simulated_states[:, 0] = ego_state_to_state_array(initial_ego_state)
52
+
53
+ # timing objects
54
+ current_time_point = initial_ego_state.time_point
55
+ delta_time_point = TimeDuration.from_s(self.proposal_sampling.interval_length)
56
+
57
+ current_iteration = SimulationIteration(current_time_point, 0)
58
+ next_iteration = SimulationIteration(current_time_point + delta_time_point, 1)
59
+
60
+ for time_idx in range(1, self.proposal_sampling.num_poses + 1):
61
+ sampling_time: TimePoint = next_iteration.time_point - current_iteration.time_point
62
+
63
+ command_states = self._tracker.track_trajectory(
64
+ current_iteration,
65
+ next_iteration,
66
+ simulated_states[:, time_idx - 1],
67
+ )
68
+
69
+ simulated_states[:, time_idx] = self._motion_model.propagate_state(
70
+ states=simulated_states[:, time_idx - 1],
71
+ command_states=command_states,
72
+ sampling_time=sampling_time,
73
+ )
74
+
75
+ current_iteration = next_iteration
76
+ next_iteration = SimulationIteration(current_iteration.time_point + delta_time_point, 1 + time_idx)
77
+
78
+ return simulated_states
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/__init__.py ADDED
File without changes
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/graph_search/__init__.py ADDED
File without changes
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/graph_search/bfs_roadblock.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import deque
2
+ from typing import Dict, List, Optional, Tuple, Union
3
+
4
+ from nuplan.common.maps.abstract_map import AbstractMap
5
+ from nuplan.common.maps.abstract_map_objects import RoadBlockGraphEdgeMapObject
6
+
7
+
8
+ class BreadthFirstSearchRoadBlock:
9
+ """
10
+ A class that performs iterative breadth first search. The class operates on the roadblock graph.
11
+ """
12
+
13
+ def __init__(
14
+ self,
15
+ start_roadblock_id: int,
16
+ map_api: Optional[AbstractMap],
17
+ forward_search: str = True,
18
+ ):
19
+ """
20
+ Constructor of BreadthFirstSearchRoadBlock class
21
+ :param start_roadblock_id: roadblock id where graph starts
22
+ :param map_api: map class in nuPlan
23
+ :param forward_search: whether to search in driving direction, defaults to True
24
+ """
25
+ self._map_api: Optional[AbstractMap] = map_api
26
+ self._queue = deque([self.id_to_roadblock(start_roadblock_id), None])
27
+ self._parent: Dict[str, Optional[RoadBlockGraphEdgeMapObject]] = dict()
28
+ self._forward_search = forward_search
29
+
30
+ # lazy loaded
31
+ self._target_roadblock_ids: List[str] = None
32
+
33
+ def search(
34
+ self, target_roadblock_id: Union[str, List[str]], max_depth: int
35
+ ) -> Tuple[List[RoadBlockGraphEdgeMapObject], bool]:
36
+ """
37
+ Apply BFS to find route to target roadblock.
38
+ :param target_roadblock_id: id of target roadblock
39
+ :param max_depth: maximum search depth
40
+ :return: tuple of route and whether a path was found
41
+ """
42
+
43
+ if isinstance(target_roadblock_id, str):
44
+ target_roadblock_id = [target_roadblock_id]
45
+ self._target_roadblock_ids = target_roadblock_id
46
+
47
+ start_edge = self._queue[0]
48
+
49
+ # Initial search states
50
+ path_found: bool = False
51
+ end_edge: RoadBlockGraphEdgeMapObject = start_edge
52
+ end_depth: int = 1
53
+ depth: int = 1
54
+
55
+ self._parent[start_edge.id + f"_{depth}"] = None
56
+
57
+ while self._queue:
58
+ current_edge = self._queue.popleft()
59
+
60
+ # Early exit condition
61
+ if self._check_end_condition(depth, max_depth):
62
+ break
63
+
64
+ # Depth tracking
65
+ if current_edge is None:
66
+ depth += 1
67
+ self._queue.append(None)
68
+ if self._queue[0] is None:
69
+ break
70
+ continue
71
+
72
+ # Goal condition
73
+ if self._check_goal_condition(current_edge, depth, max_depth):
74
+ end_edge = current_edge
75
+ end_depth = depth
76
+ path_found = True
77
+ break
78
+
79
+ neighbors = current_edge.outgoing_edges if self._forward_search else current_edge.incoming_edges
80
+
81
+ # Populate queue
82
+ for next_edge in neighbors:
83
+ # if next_edge.id in self._candidate_lane_edge_ids_old:
84
+ self._queue.append(next_edge)
85
+ self._parent[next_edge.id + f"_{depth + 1}"] = current_edge
86
+ end_edge = next_edge
87
+ end_depth = depth + 1
88
+
89
+ return self._construct_path(end_edge, end_depth), path_found
90
+
91
+ def id_to_roadblock(self, id: str) -> RoadBlockGraphEdgeMapObject:
92
+ """
93
+ Retrieves roadblock from map-api based on id
94
+ :param id: id of roadblock
95
+ :return: roadblock class
96
+ """
97
+ block = self._map_api._get_roadblock(id)
98
+ block = block or self._map_api._get_roadblock_connector(id)
99
+ return block
100
+
101
+ @staticmethod
102
+ def _check_end_condition(depth: int, max_depth: int) -> bool:
103
+ """
104
+ Check if the search should end regardless if the goal condition is met.
105
+ :param depth: The current depth to check.
106
+ :param target_depth: The target depth to check against.
107
+ :return: whether depth exceeds the target depth.
108
+ """
109
+ return depth > max_depth
110
+
111
+ def _check_goal_condition(
112
+ self,
113
+ current_edge: RoadBlockGraphEdgeMapObject,
114
+ depth: int,
115
+ max_depth: int,
116
+ ) -> bool:
117
+ """
118
+ Check if the current edge is at the target roadblock at the given depth.
119
+ :param current_edge: edge to check.
120
+ :param depth: current depth to check.
121
+ :param max_depth: maximum depth the edge should be at.
122
+ :return: True if the lane edge is contain the in the target roadblock. False, otherwise.
123
+ """
124
+ return current_edge.id in self._target_roadblock_ids and depth <= max_depth
125
+
126
+ def _construct_path(self, end_edge: RoadBlockGraphEdgeMapObject, depth: int) -> List[RoadBlockGraphEdgeMapObject]:
127
+ """
128
+ Constructs a path when goal was found.
129
+ :param end_edge: The end edge to start back propagating back to the start edge.
130
+ :param depth: The depth of the target edge.
131
+ :return: The constructed path as a list of RoadBlockGraphEdgeMapObject
132
+ """
133
+ path = [end_edge]
134
+ path_id = [end_edge.id]
135
+
136
+ while self._parent[end_edge.id + f"_{depth}"] is not None:
137
+ path.append(self._parent[end_edge.id + f"_{depth}"])
138
+ path_id.append(path[-1].id)
139
+ end_edge = self._parent[end_edge.id + f"_{depth}"]
140
+ depth -= 1
141
+
142
+ if self._forward_search:
143
+ path.reverse()
144
+ path_id.reverse()
145
+
146
+ return (path, path_id)
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/graph_search/dijkstra.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Optional, Tuple
2
+
3
+ import numpy as np
4
+ from nuplan.common.maps.abstract_map_objects import LaneGraphEdgeMapObject, RoadBlockGraphEdgeMapObject
5
+
6
+
7
+ class Dijkstra:
8
+ """
9
+ A class that performs dijkstra's shortest path. The class operates on lane level graph search.
10
+ The goal condition is specified to be if the lane can be found at the target roadblock or roadblock connector.
11
+ """
12
+
13
+ def __init__(self, start_edge: LaneGraphEdgeMapObject, candidate_lane_edge_ids: List[str]):
14
+ """
15
+ Constructor for the Dijkstra class.
16
+ :param start_edge: The starting edge for the search
17
+ :param candidate_lane_edge_ids: The candidates lane ids that can be included in the search.
18
+ """
19
+ self._queue = list([start_edge])
20
+ self._parent: Dict[str, Optional[LaneGraphEdgeMapObject]] = dict()
21
+ self._candidate_lane_edge_ids = candidate_lane_edge_ids
22
+
23
+ def search(self, target_roadblock: RoadBlockGraphEdgeMapObject) -> Tuple[List[LaneGraphEdgeMapObject], bool]:
24
+ """
25
+ Performs dijkstra's shortest path to find a route to the target roadblock.
26
+ :param target_roadblock: The target roadblock the path should end at.
27
+ :return:
28
+ - A route starting from the given start edge
29
+ - A bool indicating if the route is successfully found. Successful means that there exists a path
30
+ from the start edge to an edge contained in the end roadblock.
31
+ If unsuccessful the shortest deepest path is returned.
32
+ """
33
+ start_edge = self._queue[0]
34
+
35
+ # Initial search states
36
+ path_found: bool = False
37
+ end_edge: LaneGraphEdgeMapObject = start_edge
38
+
39
+ self._parent[start_edge.id] = None
40
+ self._frontier = [start_edge.id]
41
+ self._dist = [1]
42
+ self._depth = [1]
43
+
44
+ self._expanded = []
45
+ self._expanded_id = []
46
+ self._expanded_dist = []
47
+ self._expanded_depth = []
48
+
49
+ while len(self._queue) > 0:
50
+ dist, idx = min((val, idx) for (idx, val) in enumerate(self._dist))
51
+ current_edge = self._queue[idx]
52
+ current_depth = self._depth[idx]
53
+
54
+ del self._dist[idx], self._queue[idx], self._frontier[idx], self._depth[idx]
55
+
56
+ if self._check_goal_condition(current_edge, target_roadblock):
57
+ end_edge = current_edge
58
+ path_found = True
59
+ break
60
+
61
+ self._expanded.append(current_edge)
62
+ self._expanded_id.append(current_edge.id)
63
+ self._expanded_dist.append(dist)
64
+ self._expanded_depth.append(current_depth)
65
+
66
+ # Populate queue
67
+ for next_edge in current_edge.outgoing_edges:
68
+ if next_edge.id not in self._candidate_lane_edge_ids:
69
+ continue
70
+
71
+ alt = dist + self._edge_cost(next_edge)
72
+ if next_edge.id not in self._expanded_id and next_edge.id not in self._frontier:
73
+ self._parent[next_edge.id] = current_edge
74
+ self._queue.append(next_edge)
75
+ self._frontier.append(next_edge.id)
76
+ self._dist.append(alt)
77
+ self._depth.append(current_depth + 1)
78
+ end_edge = next_edge
79
+
80
+ elif next_edge.id in self._frontier:
81
+ next_edge_idx = self._frontier.index(next_edge.id)
82
+ current_cost = self._dist[next_edge_idx]
83
+ if alt < current_cost:
84
+ self._parent[next_edge.id] = current_edge
85
+ self._dist[next_edge_idx] = alt
86
+ self._depth[next_edge_idx] = current_depth + 1
87
+
88
+ if not path_found:
89
+ # filter max depth
90
+ max_depth = max(self._expanded_depth)
91
+ idx_max_depth = list(np.where(np.array(self._expanded_depth) == max_depth)[0])
92
+ dist_at_max_depth = [self._expanded_dist[i] for i in idx_max_depth]
93
+
94
+ dist, _idx = min((val, idx) for (idx, val) in enumerate(dist_at_max_depth))
95
+ end_edge = self._expanded[idx_max_depth[_idx]]
96
+
97
+ return self._construct_path(end_edge), path_found
98
+
99
+ @staticmethod
100
+ def _edge_cost(lane: LaneGraphEdgeMapObject) -> float:
101
+ """
102
+ Edge cost of given lane.
103
+ :param lane: lane class
104
+ :return: length of lane
105
+ """
106
+ return lane.baseline_path.length
107
+
108
+ @staticmethod
109
+ def _check_end_condition(depth: int, target_depth: int) -> bool:
110
+ """
111
+ Check if the search should end regardless if the goal condition is met.
112
+ :param depth: The current depth to check.
113
+ :param target_depth: The target depth to check against.
114
+ :return: True if:
115
+ - The current depth exceeds the target depth.
116
+ """
117
+ return depth > target_depth
118
+
119
+ @staticmethod
120
+ def _check_goal_condition(
121
+ current_edge: LaneGraphEdgeMapObject,
122
+ target_roadblock: RoadBlockGraphEdgeMapObject,
123
+ ) -> bool:
124
+ """
125
+ Check if the current edge is at the target roadblock at the given depth.
126
+ :param current_edge: The edge to check.
127
+ :param target_roadblock: The target roadblock the edge should be contained in.
128
+ :return: whether the current edge is in the target roadblock
129
+ """
130
+ return current_edge.get_roadblock_id() == target_roadblock.id
131
+
132
+ def _construct_path(self, end_edge: LaneGraphEdgeMapObject) -> List[LaneGraphEdgeMapObject]:
133
+ """
134
+ :param end_edge: The end edge to start back propagating back to the start edge.
135
+ :param depth: The depth of the target edge.
136
+ :return: The constructed path as a list of LaneGraphEdgeMapObject
137
+ """
138
+ path = [end_edge]
139
+ while self._parent[end_edge.id] is not None:
140
+ node = self._parent[end_edge.id]
141
+ path.append(node)
142
+ end_edge = node
143
+ path.reverse()
144
+
145
+ return path
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_array_representation.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TODO: Move & rename this file for common usage (not specific for PDM)
2
+ from typing import List
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ import shapely
7
+ from nuplan.common.actor_state.ego_state import EgoState
8
+ from nuplan.common.actor_state.state_representation import StateSE2, StateVector2D, TimePoint
9
+ from nuplan.common.actor_state.vehicle_parameters import VehicleParameters
10
+
11
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import (
12
+ BBCoordsIndex,
13
+ PointIndex,
14
+ SE2Index,
15
+ StateIndex,
16
+ )
17
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_geometry_utils import (
18
+ get_acceleration_shifted,
19
+ get_velocity_shifted,
20
+ se2_array_translate_longitudinally,
21
+ translate_lon_and_lat,
22
+ )
23
+
24
+
25
+ def array_to_state_se2(array: npt.NDArray[np.float64]) -> StateSE2:
26
+ """
27
+ Converts array representation to single StateSE2.
28
+ :param array: array filled with (x,y,θ)
29
+ :return: StateSE2 class
30
+ """
31
+ return StateSE2(array[0], array[1], array[2])
32
+
33
+
34
+ # use numpy vectorize function to apply on last dim
35
+ array_to_state_se2_vectorize = np.vectorize(array_to_state_se2, signature="(n)->()")
36
+
37
+
38
+ def array_to_states_se2(array: npt.NDArray[np.float64]) -> npt.NDArray[np.object_]:
39
+ """
40
+ Converts array representation to StateSE2 over last dim.
41
+ :param array: array filled with (x,y,θ) on last dim
42
+ :return: array of StateSE2 class
43
+ """
44
+ assert array.shape[-1] == len(SE2Index)
45
+ return array_to_state_se2_vectorize(array)
46
+
47
+
48
+ def state_se2_to_array(state_se2: StateSE2) -> npt.NDArray[np.float64]:
49
+ """
50
+ Converts StateSE2 to array representation.
51
+ :param state_se2: class containing (x,y,θ)
52
+ :return: array containing (x,y,θ)
53
+ """
54
+ array = np.zeros(len(SE2Index), dtype=np.float64)
55
+ array[SE2Index.X] = state_se2.x
56
+ array[SE2Index.Y] = state_se2.y
57
+ array[SE2Index.HEADING] = state_se2.heading
58
+ return array
59
+
60
+
61
+ def states_se2_to_array(states_se2: List[StateSE2]) -> npt.NDArray[np.float64]:
62
+ """
63
+ Converts list of StateSE2 object to array representation
64
+ :param states_se2: list of StateSE2 object's
65
+ :return: array representation of states
66
+ """
67
+ state_se2_array = np.zeros((len(states_se2), len(SE2Index)), dtype=np.float64)
68
+ for i, state_se2 in enumerate(states_se2):
69
+ state_se2_array[i] = state_se2_to_array(state_se2)
70
+ return state_se2_array
71
+
72
+
73
+ def ego_state_to_state_array(ego_state: EgoState) -> npt.NDArray[np.float64]:
74
+ """
75
+ Converts an ego state into an array representation (drops time-stamps and vehicle parameters)
76
+ :param ego_state: ego state class
77
+ :return: array containing ego state values (from the rear axle)
78
+ """
79
+ state_array = np.zeros(StateIndex.size(), dtype=np.float64)
80
+
81
+ state_array[StateIndex.STATE_SE2] = ego_state.rear_axle.serialize()
82
+ state_array[StateIndex.VELOCITY_2D] = ego_state.dynamic_car_state.rear_axle_velocity_2d.array
83
+ state_array[StateIndex.ACCELERATION_2D] = ego_state.dynamic_car_state.rear_axle_acceleration_2d.array
84
+
85
+ state_array[StateIndex.STEERING_ANGLE] = ego_state.tire_steering_angle
86
+ state_array[StateIndex.STEERING_RATE] = ego_state.dynamic_car_state.tire_steering_rate
87
+
88
+ state_array[StateIndex.ANGULAR_VELOCITY] = ego_state.dynamic_car_state.angular_velocity
89
+ state_array[StateIndex.ANGULAR_ACCELERATION] = ego_state.dynamic_car_state.angular_acceleration
90
+
91
+ return state_array
92
+
93
+
94
+ def ego_state_to_center_state_array(ego_state: EgoState) -> npt.NDArray[np.float64]:
95
+ """
96
+ Converts an ego state into an array representation (drops time-stamps and vehicle parameters)
97
+ :param ego_state: ego state class
98
+ :return: array containing ego state values (from the center)
99
+ """
100
+ state_array = np.zeros(StateIndex.size(), dtype=np.float64)
101
+
102
+ state_array[StateIndex.STATE_SE2] = ego_state.center.serialize()
103
+ state_array[StateIndex.VELOCITY_2D] = ego_state.dynamic_car_state.center_velocity_2d.array
104
+ state_array[StateIndex.ACCELERATION_2D] = ego_state.dynamic_car_state.center_acceleration_2d.array
105
+
106
+ state_array[StateIndex.STEERING_ANGLE] = ego_state.tire_steering_angle
107
+ state_array[StateIndex.STEERING_RATE] = ego_state.dynamic_car_state.tire_steering_rate
108
+
109
+ state_array[StateIndex.ANGULAR_VELOCITY] = ego_state.dynamic_car_state.angular_velocity
110
+ state_array[StateIndex.ANGULAR_ACCELERATION] = ego_state.dynamic_car_state.angular_acceleration
111
+
112
+ return state_array
113
+
114
+
115
+ def ego_states_to_state_array(ego_states: List[EgoState]) -> npt.NDArray[np.float64]:
116
+ """
117
+ Converts a list of ego states into an array representation (drops time-stamps and vehicle parameters)
118
+ :param ego_state: ego state class
119
+ :return: array containing ego state values (from the rear axle)
120
+ """
121
+ state_array = np.array(
122
+ [ego_state_to_state_array(ego_state) for ego_state in ego_states],
123
+ dtype=np.float64,
124
+ )
125
+ return state_array
126
+
127
+
128
+ def ego_states_to_center_state_array(
129
+ ego_states: List[EgoState],
130
+ ) -> npt.NDArray[np.float64]:
131
+ """
132
+ Converts a list of ego states into an center array representation (drops time-stamps and vehicle parameters)
133
+ :param ego_state: ego state class
134
+ :return: array containing ego state values (from the center)
135
+ """
136
+ state_array = np.array(
137
+ [ego_state_to_center_state_array(ego_state) for ego_state in ego_states],
138
+ dtype=np.float64,
139
+ )
140
+ return state_array
141
+
142
+
143
+ def state_array_to_ego_state(
144
+ state_array: npt.NDArray[np.float64],
145
+ time_point: TimePoint,
146
+ vehicle_parameters: VehicleParameters,
147
+ ) -> EgoState:
148
+ """
149
+ Converts array representation of ego state back to ego state class.
150
+ :param state_array: array representation of ego states
151
+ :param time_point: time point of state
152
+ :param vehicle_parameters: vehicle parameter of ego
153
+ :return: nuPlan's EgoState object
154
+ """
155
+ return EgoState.build_from_rear_axle(
156
+ rear_axle_pose=StateSE2(*state_array[StateIndex.STATE_SE2]),
157
+ rear_axle_velocity_2d=StateVector2D(*state_array[StateIndex.VELOCITY_2D]),
158
+ rear_axle_acceleration_2d=StateVector2D(*state_array[StateIndex.ACCELERATION_2D]),
159
+ tire_steering_angle=state_array[StateIndex.STEERING_ANGLE],
160
+ time_point=time_point,
161
+ vehicle_parameters=vehicle_parameters,
162
+ is_in_auto_mode=True,
163
+ angular_vel=state_array[StateIndex.ANGULAR_VELOCITY],
164
+ angular_accel=state_array[StateIndex.ANGULAR_ACCELERATION],
165
+ tire_steering_rate=state_array[StateIndex.STEERING_RATE],
166
+ )
167
+
168
+
169
+ def state_array_to_ego_states(
170
+ state_array: npt.NDArray[np.float64],
171
+ time_points: List[TimePoint],
172
+ vehicle_parameter: VehicleParameters,
173
+ ) -> List[EgoState]:
174
+ """
175
+ Converts array representation of ego states back to list of ego state class.
176
+ :param state_array: array representation of ego states
177
+ :param time_point: list of time point of state array
178
+ :param vehicle_parameters: vehicle parameter of ego
179
+ :return: list nuPlan's EgoState object
180
+ """
181
+ ego_states_list: List[EgoState] = []
182
+ for i, time_point in enumerate(time_points):
183
+ state = state_array[i] if i < len(state_array) else state_array[-1]
184
+ ego_states_list.append(state_array_to_ego_state(state, time_point, vehicle_parameter))
185
+ return ego_states_list
186
+
187
+
188
+ def state_array_to_coords_array(
189
+ states: npt.NDArray[np.float64],
190
+ vehicle_parameters: VehicleParameters,
191
+ ) -> npt.NDArray[np.float64]:
192
+ """
193
+ Converts multi-dim array representation of ego states to bounding box coordinates
194
+ :param state_array: array representation of ego states
195
+ :param vehicle_parameters: vehicle parameter of ego
196
+ :return: multi-dim array bounding box coordinates
197
+ """
198
+ n_batch, n_time, n_states = states.shape
199
+
200
+ half_length, half_width, rear_axle_to_center = (
201
+ vehicle_parameters.half_length,
202
+ vehicle_parameters.half_width,
203
+ vehicle_parameters.rear_axle_to_center,
204
+ )
205
+
206
+ headings = states[..., StateIndex.HEADING]
207
+ cos, sin = np.cos(headings), np.sin(headings)
208
+
209
+ # calculate ego center from rear axle
210
+ rear_axle_to_center_translate = np.stack([rear_axle_to_center * cos, rear_axle_to_center * sin], axis=-1)
211
+
212
+ ego_centers: npt.NDArray[np.float64] = states[..., StateIndex.POINT] + rear_axle_to_center_translate
213
+
214
+ coords_array: npt.NDArray[np.float64] = np.zeros((n_batch, n_time, len(BBCoordsIndex), 2), dtype=np.float64)
215
+
216
+ coords_array[:, :, BBCoordsIndex.CENTER] = ego_centers
217
+
218
+ coords_array[:, :, BBCoordsIndex.FRONT_LEFT] = translate_lon_and_lat(ego_centers, headings, half_length, half_width)
219
+ coords_array[:, :, BBCoordsIndex.FRONT_RIGHT] = translate_lon_and_lat(
220
+ ego_centers, headings, half_length, -half_width
221
+ )
222
+ coords_array[:, :, BBCoordsIndex.REAR_LEFT] = translate_lon_and_lat(ego_centers, headings, -half_length, half_width)
223
+ coords_array[:, :, BBCoordsIndex.REAR_RIGHT] = translate_lon_and_lat(
224
+ ego_centers, headings, -half_length, -half_width
225
+ )
226
+
227
+ return coords_array
228
+
229
+
230
+ def coords_array_to_polygon_array(
231
+ coords: npt.NDArray[np.float64],
232
+ ) -> npt.NDArray[np.object_]:
233
+ """
234
+ Converts multi-dim array of bounding box coords of to polygons
235
+ :param coords: bounding box coords (including corners and center)
236
+ :return: array of shapely's polygons
237
+ """
238
+ # create coords copy and use center point for closed exterior
239
+ coords_exterior: npt.NDArray[np.float64] = coords.copy()
240
+ coords_exterior[..., BBCoordsIndex.CENTER, :] = coords_exterior[..., BBCoordsIndex.FRONT_LEFT, :]
241
+
242
+ # load new coordinates into polygon array
243
+ polygons = shapely.creation.polygons(coords_exterior)
244
+
245
+ return polygons
246
+
247
+
248
+ def state_array_to_center_state_array(
249
+ state_array: npt.NDArray[np.float64], vehicle_parameters: VehicleParameters
250
+ ) -> npt.NDArray[np.float64]:
251
+ assert state_array.shape[-1] == StateIndex.size()
252
+
253
+ center_states = np.zeros(state_array.shape, dtype=np.float64)
254
+
255
+ # coordinates
256
+ center_states[..., StateIndex.STATE_SE2] = se2_array_translate_longitudinally(
257
+ state_array[..., StateIndex.STATE_SE2], vehicle_parameters.rear_axle_to_center
258
+ )
259
+
260
+ # velocity & acceleration
261
+ displacement = np.zeros((1, 2), dtype=np.float64)
262
+ displacement[..., PointIndex.X] = vehicle_parameters.rear_axle_to_center
263
+
264
+ center_states[..., StateIndex.VELOCITY_2D] = get_velocity_shifted(
265
+ displacement,
266
+ state_array[..., StateIndex.VELOCITY_2D],
267
+ state_array[..., StateIndex.ANGULAR_VELOCITY],
268
+ )
269
+ center_states[..., StateIndex.ACCELERATION_2D] = get_acceleration_shifted(
270
+ displacement,
271
+ state_array[..., StateIndex.ACCELERATION_2D],
272
+ state_array[..., StateIndex.ANGULAR_VELOCITY],
273
+ state_array[..., StateIndex.ANGULAR_ACCELERATION],
274
+ )
275
+
276
+ # rest is copied
277
+ center_states[..., StateIndex.STEERING_ANGLE] = state_array[..., StateIndex.STEERING_ANGLE]
278
+ center_states[..., StateIndex.STEERING_RATE] = state_array[..., StateIndex.STEERING_RATE]
279
+ center_states[..., StateIndex.ANGULAR_VELOCITY] = state_array[..., StateIndex.ANGULAR_VELOCITY]
280
+ center_states[..., StateIndex.ANGULAR_ACCELERATION] = state_array[..., StateIndex.ANGULAR_ACCELERATION]
281
+
282
+ return center_states
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_emergency_brake.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ from nuplan.common.actor_state.ego_state import EgoState
6
+ from nuplan.common.actor_state.state_representation import StateSE2, StateVector2D, TimePoint
7
+ from nuplan.common.geometry.convert import relative_to_absolute_poses
8
+ from nuplan.planning.simulation.trajectory.interpolated_trajectory import InterpolatedTrajectory
9
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
10
+
11
+ from navsim.planning.simulation.planner.pdm_planner.scoring.pdm_scorer import PDMScorer
12
+
13
+
14
+ class PDMEmergencyBrake:
15
+ """Class for emergency brake maneuver of PDM-Closed."""
16
+
17
+ def __init__(
18
+ self,
19
+ trajectory_sampling: TrajectorySampling,
20
+ time_to_infraction_threshold: float = 2.0,
21
+ max_ego_speed: float = 5.0,
22
+ max_long_accel: float = 2.40,
23
+ min_long_accel: float = -4.05,
24
+ infraction: str = "collision",
25
+ ):
26
+ """
27
+ Constructor for PDMEmergencyBrake
28
+ :param trajectory_sampling: Sampling parameters for final trajectory
29
+ :param time_to_infraction_threshold: threshold for applying brake, defaults to 2.0
30
+ :param max_ego_speed: maximum speed to apply brake, defaults to 5.0
31
+ :param max_long_accel: maximum longitudinal acceleration for braking, defaults to 2.40
32
+ :param min_long_accel: min longitudinal acceleration for braking, defaults to -4.05
33
+ :param infraction: infraction to determine braking (collision or ttc), defaults to "collision"
34
+ """
35
+
36
+ # trajectory parameters
37
+ self._trajectory_sampling = trajectory_sampling
38
+
39
+ # braking parameters
40
+ self._max_ego_speed: float = max_ego_speed # [m/s]
41
+ self._max_long_accel: float = max_long_accel # [m/s^2]
42
+ self._min_long_accel: float = min_long_accel # [m/s^2]
43
+
44
+ # braking condition parameters
45
+ self._time_to_infraction_threshold: float = time_to_infraction_threshold
46
+ self._infraction: str = infraction
47
+
48
+ assert self._infraction in [
49
+ "collision",
50
+ "ttc",
51
+ ], f"PDMEmergencyBraking: Infraction {self._infraction} not available as brake condition!"
52
+
53
+ def brake_if_emergency(
54
+ self, ego_state: EgoState, scores: npt.NDArray[np.float64], scorer: PDMScorer
55
+ ) -> Optional[InterpolatedTrajectory]:
56
+ """
57
+ Applies emergency brake only if an infraction is expected within horizon.
58
+ :param ego_state: state object of ego
59
+ :param scores: array of proposal scores
60
+ :param metric: scorer class of PDM
61
+ :return: brake trajectory or None
62
+ """
63
+
64
+ trajectory = None
65
+ ego_speed: float = ego_state.dynamic_car_state.speed
66
+
67
+ proposal_idx = np.argmax(scores)
68
+
69
+ # retrieve time to infraction depending on brake detection mode
70
+ if self._infraction == "ttc":
71
+ time_to_infraction = scorer.time_to_ttc_infraction(proposal_idx)
72
+
73
+ elif self._infraction == "collision":
74
+ time_to_infraction = scorer.time_to_at_fault_collision(proposal_idx)
75
+
76
+ # check time to infraction below threshold
77
+ if time_to_infraction <= self._time_to_infraction_threshold and ego_speed <= self._max_ego_speed:
78
+ trajectory = self._generate_trajectory(ego_state)
79
+
80
+ return trajectory
81
+
82
+ def _generate_trajectory(self, ego_state: EgoState) -> InterpolatedTrajectory:
83
+ """
84
+ Generates trajectory for reach zero velocity.
85
+ :param ego_state: state object of ego
86
+ :return: InterpolatedTrajectory for braking
87
+ """
88
+ current_time_point = ego_state.time_point
89
+ current_velocity = ego_state.dynamic_car_state.center_velocity_2d.x
90
+ current_acceleration = ego_state.dynamic_car_state.center_acceleration_2d.x
91
+
92
+ target_velocity = 0.0
93
+
94
+ if current_velocity > 0.2:
95
+ k_p = 10.0
96
+ k_d = 0.0
97
+
98
+ error = -current_velocity
99
+ dt_error = -current_acceleration
100
+ u_t = k_p * error + k_d * dt_error
101
+
102
+ error = max(min(u_t, self._max_long_accel), self._min_long_accel)
103
+ correcting_velocity = 11 / 10 * (current_velocity + error)
104
+
105
+ else:
106
+ k_p = 4
107
+ k_d = 1
108
+
109
+ error = target_velocity - current_velocity
110
+ dt_error = -current_acceleration
111
+
112
+ u_t = k_p * error + k_d * dt_error
113
+
114
+ correcting_velocity = max(min(u_t, self._max_long_accel), self._min_long_accel)
115
+
116
+ trajectory_states = []
117
+
118
+ # Propagate planned trajectory for set number of samples
119
+ for sample in range(self._trajectory_sampling.num_poses + 1):
120
+ time_t = self._trajectory_sampling.interval_length * sample
121
+ pose = relative_to_absolute_poses(ego_state.center, [StateSE2(correcting_velocity * time_t, 0, 0)])[0]
122
+
123
+ ego_state_ = EgoState.build_from_center(
124
+ center=pose,
125
+ center_velocity_2d=StateVector2D(0, 0),
126
+ center_acceleration_2d=StateVector2D(0, 0),
127
+ tire_steering_angle=0.0,
128
+ time_point=current_time_point,
129
+ vehicle_parameters=ego_state.car_footprint.vehicle_parameters,
130
+ )
131
+ trajectory_states.append(ego_state_)
132
+
133
+ current_time_point += TimePoint(int(self._trajectory_sampling.interval_length * 1e6))
134
+
135
+ return InterpolatedTrajectory(trajectory_states)
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_enums.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TODO: Move & rename this file for common usage (not specific for PDM)
2
+ # TODO: Remove @classmethod + @property decorators (deprecated in Python 3.13)
3
+ from enum import IntEnum
4
+
5
+
6
+ class StateIndex:
7
+ """Index mapping for array representation of ego states."""
8
+
9
+ _X = 0
10
+ _Y = 1
11
+ _HEADING = 2
12
+ _VELOCITY_X = 3
13
+ _VELOCITY_Y = 4
14
+ _ACCELERATION_X = 5
15
+ _ACCELERATION_Y = 6
16
+ _STEERING_ANGLE = 7
17
+ _STEERING_RATE = 8
18
+ _ANGULAR_VELOCITY = 9
19
+ _ANGULAR_ACCELERATION = 10
20
+
21
+ @classmethod
22
+ def size(cls):
23
+ valid_attributes = [
24
+ attribute
25
+ for attribute in dir(cls)
26
+ if attribute.startswith("_") and not attribute.startswith("__") and not callable(getattr(cls, attribute))
27
+ ]
28
+ return len(valid_attributes)
29
+
30
+ @classmethod
31
+ @property
32
+ def X(cls):
33
+ return cls._X
34
+
35
+ @classmethod
36
+ @property
37
+ def Y(cls):
38
+ return cls._Y
39
+
40
+ @classmethod
41
+ @property
42
+ def HEADING(cls):
43
+ return cls._HEADING
44
+
45
+ @classmethod
46
+ @property
47
+ def VELOCITY_X(cls):
48
+ return cls._VELOCITY_X
49
+
50
+ @classmethod
51
+ @property
52
+ def VELOCITY_Y(cls):
53
+ return cls._VELOCITY_Y
54
+
55
+ @classmethod
56
+ @property
57
+ def ACCELERATION_X(cls):
58
+ return cls._ACCELERATION_X
59
+
60
+ @classmethod
61
+ @property
62
+ def ACCELERATION_Y(cls):
63
+ return cls._ACCELERATION_Y
64
+
65
+ @classmethod
66
+ @property
67
+ def STEERING_ANGLE(cls):
68
+ return cls._STEERING_ANGLE
69
+
70
+ @classmethod
71
+ @property
72
+ def STEERING_RATE(cls):
73
+ return cls._STEERING_RATE
74
+
75
+ @classmethod
76
+ @property
77
+ def ANGULAR_VELOCITY(cls):
78
+ return cls._ANGULAR_VELOCITY
79
+
80
+ @classmethod
81
+ @property
82
+ def ANGULAR_ACCELERATION(cls):
83
+ return cls._ANGULAR_ACCELERATION
84
+
85
+ @classmethod
86
+ @property
87
+ def POINT(cls):
88
+ # assumes X, Y have subsequent indices
89
+ return slice(cls._X, cls._Y + 1)
90
+
91
+ @classmethod
92
+ @property
93
+ def STATE_SE2(cls):
94
+ # assumes X, Y, HEADING have subsequent indices
95
+ return slice(cls._X, cls._HEADING + 1)
96
+
97
+ @classmethod
98
+ @property
99
+ def VELOCITY_2D(cls):
100
+ # assumes velocity X, Y have subsequent indices
101
+ return slice(cls._VELOCITY_X, cls._VELOCITY_Y + 1)
102
+
103
+ @classmethod
104
+ @property
105
+ def ACCELERATION_2D(cls):
106
+ # assumes acceleration X, Y have subsequent indices
107
+ return slice(cls._ACCELERATION_X, cls._ACCELERATION_Y + 1)
108
+
109
+
110
+ class PointIndex(IntEnum):
111
+ """Index mapping for (x,y) arrays."""
112
+
113
+ X = 0
114
+ Y = 1
115
+
116
+
117
+ class SE2Index(IntEnum):
118
+ """Index mapping for state se2 (x,y,θ) arrays."""
119
+
120
+ X = 0
121
+ Y = 1
122
+ HEADING = 2
123
+
124
+
125
+ class DynamicStateIndex(IntEnum):
126
+ """Index mapping for dynamic car state (output of controller)."""
127
+
128
+ ACCELERATION_X = 0
129
+ STEERING_RATE = 1
130
+
131
+
132
+ class StateIDMIndex(IntEnum):
133
+ """Index mapping for IDM states."""
134
+
135
+ PROGRESS = 0
136
+ VELOCITY = 1
137
+
138
+
139
+ class LeadingAgentIndex(IntEnum):
140
+ """Index mapping for leading agent state (for IDM policies)."""
141
+
142
+ PROGRESS = 0
143
+ VELOCITY = 1
144
+ LENGTH_REAR = 2
145
+
146
+
147
+ class BBCoordsIndex(IntEnum):
148
+ """Index mapping for corners and center of bounding boxes."""
149
+
150
+ FRONT_LEFT = 0
151
+ REAR_LEFT = 1
152
+ REAR_RIGHT = 2
153
+ FRONT_RIGHT = 3
154
+ CENTER = 4
155
+
156
+
157
+ class EgoAreaIndex(IntEnum):
158
+ """Index mapping for area of ego agent (used in PDMScorer)."""
159
+
160
+ MULTIPLE_LANES = 0
161
+ NON_DRIVABLE_AREA = 1
162
+ ONCOMING_TRAFFIC = 2
163
+
164
+
165
+ class MultiMetricIndex(IntEnum):
166
+ """Index mapping multiplicative metrics (used in PDMScorer)."""
167
+
168
+ NO_COLLISION = 0
169
+ DRIVABLE_AREA = 1
170
+ TRAFFIC_LIGHT_COMPLIANCE = 2
171
+ DRIVING_DIRECTION = 3
172
+
173
+
174
+ class WeightedMetricIndex(IntEnum):
175
+ """Index mapping weighted metrics (used in PDMScorer)."""
176
+
177
+ PROGRESS = 0
178
+ TTC = 1
179
+ LANE_KEEPING = 2
180
+ HISTORY_COMFORT = 3
181
+ TWO_FRAME_EXTENDED_COMFORT = 4
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_geometry_utils.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TODO: Move & rename this file for common usage (not specific for PDM)
2
+
3
+ from typing import List
4
+
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+ from nuplan.common.actor_state.state_representation import StateSE2
8
+
9
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import PointIndex, SE2Index
10
+
11
+
12
+ def normalize_angle(angle):
13
+ """
14
+ Map a angle in range [-π, π]
15
+ :param angle: any angle as float
16
+ :return: normalized angle
17
+ """
18
+ return np.arctan2(np.sin(angle), np.cos(angle))
19
+
20
+
21
+ def parallel_discrete_path(discrete_path: List[StateSE2], offset=float) -> List[StateSE2]:
22
+ """
23
+ Creates a parallel discrete path for a given offset.
24
+ :param discrete_path: baseline path (x,y,θ)
25
+ :param offset: parall loffset
26
+ :return: parallel discrete path
27
+ """
28
+ parallel_discrete_path = []
29
+ for state in discrete_path:
30
+ theta = state.heading + np.pi / 2
31
+ x_new = state.x + np.cos(theta) * offset
32
+ y_new = state.y + np.sin(theta) * offset
33
+ parallel_discrete_path.append(StateSE2(x_new, y_new, state.heading))
34
+ return parallel_discrete_path
35
+
36
+
37
+ def translate_lon_and_lat(
38
+ centers: npt.NDArray[np.float64],
39
+ headings: npt.NDArray[np.float64],
40
+ lon: float,
41
+ lat: float,
42
+ ) -> npt.NDArray[np.float64]:
43
+ """
44
+ Translate the position component of an centers point array
45
+ :param centers: array to be translated
46
+ :param headings: array with heading angles
47
+ :param lon: [m] distance by which a point should be translated in longitudinal direction
48
+ :param lat: [m] distance by which a point should be translated in lateral direction
49
+ :return array of translated coordinates
50
+ """
51
+ half_pi = np.pi / 2.0
52
+ translation: npt.NDArray[np.float64] = np.stack(
53
+ [
54
+ (lat * np.cos(headings + half_pi)) + (lon * np.cos(headings)),
55
+ (lat * np.sin(headings + half_pi)) + (lon * np.sin(headings)),
56
+ ],
57
+ axis=-1,
58
+ )
59
+ return centers + translation
60
+
61
+
62
+ def calculate_progress(path: List[StateSE2]) -> List[float]:
63
+ """
64
+ Calculate the cumulative progress of a given path.
65
+ :param path: a path consisting of StateSE2 as waypoints
66
+ :return: a cumulative list of progress
67
+ """
68
+ x_position = [point.x for point in path]
69
+ y_position = [point.y for point in path]
70
+ x_diff = np.diff(x_position)
71
+ y_diff = np.diff(y_position)
72
+ points_diff: npt.NDArray[np.float64] = np.concatenate(([x_diff], [y_diff]), axis=0, dtype=np.float64)
73
+ progress_diff = np.append(0.0, np.linalg.norm(points_diff, axis=0))
74
+ return np.cumsum(progress_diff, dtype=np.float64) # type: ignore
75
+
76
+
77
+ def convert_absolute_to_relative_se2_array(
78
+ origin: StateSE2, state_se2_array: npt.NDArray[np.float64]
79
+ ) -> npt.NDArray[np.float64]:
80
+ """
81
+ Converts an StateSE2 array from global to relative coordinates.
82
+ :param origin: origin pose of relative coords system
83
+ :param state_se2_array: array of SE2 states with (x,y,θ) in last dim
84
+ :return: SE2 coords array in relative coordinates
85
+ """
86
+ assert len(SE2Index) == state_se2_array.shape[-1]
87
+
88
+ theta = -origin.heading
89
+ origin_array = np.array([[origin.x, origin.y, origin.heading]], dtype=np.float64)
90
+
91
+ R = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])
92
+
93
+ points_rel = state_se2_array - origin_array
94
+ points_rel[..., :2] = points_rel[..., :2] @ R.T
95
+ points_rel[:, 2] = normalize_angle(points_rel[:, 2])
96
+
97
+ return points_rel
98
+
99
+
100
+ def convert_absolute_to_relative_point_array(
101
+ origin: StateSE2, point_array: npt.NDArray[np.float64]
102
+ ) -> npt.NDArray[np.float64]:
103
+ """
104
+ Converts an points array from global to relative coordinates.
105
+ :param origin: origin pose of relative coords system
106
+ :param points_array: array of points with (x,y) in last dim
107
+ :return: points coords array in relative coordinates
108
+ """
109
+ assert len(PointIndex) == point_array.shape[-1]
110
+
111
+ theta = -origin.heading
112
+ origin_array = np.array([[origin.x, origin.y, origin.heading]], dtype=np.float64)
113
+
114
+ R = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])
115
+
116
+ points_rel = point_array - origin_array[..., :2]
117
+ points_rel[..., :2] = points_rel[..., :2] @ R.T
118
+
119
+ return points_rel
120
+
121
+
122
+ def se2_array_translate_longitudinally(se2_array: npt.NDArray[np.float64], distance: float) -> npt.NDArray[np.float64]:
123
+ """
124
+ Translates an SE2 array along the heading axis by distance.
125
+ :param se2_array: array of SE2 states with (x,y,θ) in last dim
126
+ :param distance: distance to translate [m]
127
+ :return: Translated SE2 coords array.
128
+ """
129
+ assert se2_array.shape[-1] == len(SE2Index)
130
+ translate_se2 = np.zeros(se2_array.shape, dtype=np.float64)
131
+ translate_se2[..., SE2Index.X] = se2_array[..., SE2Index.X] + np.cos(se2_array[..., SE2Index.HEADING]) * distance
132
+ translate_se2[..., SE2Index.Y] = se2_array[..., SE2Index.Y] + np.sin(se2_array[..., SE2Index.HEADING]) * distance
133
+ translate_se2[..., SE2Index.HEADING] = se2_array[..., SE2Index.HEADING]
134
+ return translate_se2
135
+
136
+
137
+ def get_velocity_shifted(
138
+ displacement: npt.NDArray[np.float64],
139
+ ref_velocity_2d: npt.NDArray[np.float64],
140
+ ref_angular_vel: npt.NDArray[np.float64],
141
+ ) -> npt.NDArray[np.float64]:
142
+ """
143
+ Computes the velocity at a query point on the same planar rigid body as a reference point.
144
+ :param displacement: [m] The displacement vector from the reference to the query point
145
+ :param ref_velocity_2d: [m/s] The velocity vector at the reference point
146
+ :param ref_angular_vel: [rad/s] The angular velocity of the body around the vertical axis
147
+ :return: [m/s] The velocity vector at the given displacement.
148
+ """
149
+ assert displacement.shape[-1] == len(PointIndex)
150
+ assert ref_velocity_2d.shape[-1] == len(PointIndex)
151
+ assert ref_velocity_2d.shape[:-1] == ref_angular_vel.shape
152
+ velocity_shift_term = np.zeros(ref_velocity_2d.shape, dtype=ref_velocity_2d.dtype)
153
+ velocity_shift_term[..., PointIndex.X] = -displacement[..., PointIndex.Y] * ref_angular_vel
154
+ velocity_shift_term[..., PointIndex.Y] = displacement[..., PointIndex.X] * ref_angular_vel
155
+ return ref_velocity_2d + velocity_shift_term
156
+
157
+
158
+ def get_acceleration_shifted(
159
+ displacement: npt.NDArray[np.float64],
160
+ ref_accel_2d: npt.NDArray[np.float64],
161
+ ref_angular_vel: npt.NDArray[np.float64],
162
+ ref_angular_accel: npt.NDArray[np.float64],
163
+ ) -> npt.NDArray[np.float64]:
164
+ """
165
+ Computes the acceleration at a query point on the same planar rigid body as a reference point.
166
+ :param displacement: [m] The displacement vector from the reference to the query point
167
+ :param ref_accel_2d: [m/s^2] The acceleration vector at the reference point
168
+ :param ref_angular_vel: [rad/s] The angular velocity of the body around the vertical axis
169
+ :param ref_angular_accel: [rad/s^2] The angular acceleration of the body around the vertical axis
170
+ :return: [m/s^2] The acceleration vector at the given displacement.
171
+ """
172
+ assert displacement.shape[-1] == len(PointIndex)
173
+ assert ref_accel_2d.shape[-1] == len(PointIndex)
174
+ assert ref_accel_2d.shape[:-1] == ref_angular_vel.shape
175
+ assert ref_accel_2d.shape[:-1] == ref_angular_accel.shape
176
+ centripetal_acceleration_term = displacement * ref_angular_vel[..., None] ** 2
177
+ angular_acceleration_term = displacement * ref_angular_accel[..., None]
178
+ return ref_accel_2d + centripetal_acceleration_term + angular_acceleration_term
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_path.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TODO: Move this file for common usage (not specific for PDM)
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from typing import Any, List, Tuple, Type, Union
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ from nuplan.common.actor_state.state_representation import StateSE2
11
+ from scipy.interpolate import interp1d
12
+ from shapely.creation import linestrings
13
+ from shapely.geometry import LineString
14
+ from shapely.ops import substring
15
+
16
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_array_representation import (
17
+ array_to_states_se2,
18
+ states_se2_to_array,
19
+ )
20
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import SE2Index
21
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_geometry_utils import calculate_progress, normalize_angle
22
+
23
+
24
+ class PDMPath:
25
+ """Class representing a path to interpolate for PDM."""
26
+
27
+ def __init__(self, discrete_path: List[StateSE2]):
28
+ """
29
+ Constructor for PDMPath
30
+ :param discrete_path: list of (x,y,θ) values
31
+ """
32
+
33
+ # attribute
34
+ self._discrete_path = discrete_path
35
+
36
+ # loaded during initialization
37
+ self._states_se2_array = states_se2_to_array(discrete_path)
38
+ self._states_se2_array[:, SE2Index.HEADING] = np.unwrap(self._states_se2_array[:, SE2Index.HEADING], axis=0)
39
+ self._progress = calculate_progress(discrete_path)
40
+ self._linestring = linestrings(self._states_se2_array[:, : SE2Index.HEADING])
41
+ self._interpolator = interp1d(self._progress, self._states_se2_array, axis=0)
42
+
43
+ def __reduce__(self) -> Tuple[Type[PDMPath], Tuple[Any, ...]]:
44
+ """Helper for pickling."""
45
+ return self.__class__, (self._discrete_path,)
46
+
47
+ @property
48
+ def discrete_path(self):
49
+ """Getter for discrete StateSE2 objects of path."""
50
+ return self._discrete_path
51
+
52
+ @property
53
+ def length(self):
54
+ """Getter for length of path."""
55
+ return self._progress[-1]
56
+
57
+ @property
58
+ def linestring(self) -> LineString:
59
+ """Getter for shapely's linestring of path."""
60
+ return self._linestring
61
+
62
+ def project(self, points: Any) -> Any:
63
+ warnings.filterwarnings(
64
+ "ignore", message="invalid value encountered in line_locate_point", category=RuntimeWarning
65
+ )
66
+ return self._linestring.project(points)
67
+
68
+ def interpolate(
69
+ self,
70
+ distances: Union[List[float], npt.NDArray[np.float64]],
71
+ as_array=False,
72
+ ) -> Union[npt.NDArray[np.object_], npt.NDArray[np.float64]]:
73
+ """
74
+ Calculates (x,y,θ) for a given distance along the path.
75
+ :param distances: list of array of distance values
76
+ :param as_array: whether to return in array representation, defaults to False
77
+ :return: array of StateSE2 class or (x,y,θ) values
78
+ """
79
+ clipped_distances = np.clip(distances, 1e-5, self.length)
80
+ interpolated_se2_array = self._interpolator(clipped_distances)
81
+ interpolated_se2_array[..., 2] = normalize_angle(interpolated_se2_array[..., 2])
82
+ interpolated_se2_array[np.isnan(interpolated_se2_array)] = 0.0
83
+
84
+ if as_array:
85
+ return interpolated_se2_array
86
+
87
+ return array_to_states_se2(interpolated_se2_array)
88
+
89
+ def substring(self, start_distance: float, end_distance: float) -> LineString:
90
+ """
91
+ Creates a sub-linestring between start and ending distances.
92
+ :param start_distance: distance along the path to start [m]
93
+ :param end_distance: distance along the path to end [m]
94
+ :return: LineString
95
+ """
96
+
97
+ # try faster method fist
98
+ start_distance = np.clip(start_distance, 0.0, self.length)
99
+ end_distance = np.clip(end_distance, 0.0, self.length)
100
+ in_interval = np.logical_and(start_distance <= self._progress, self._progress <= end_distance)
101
+ coordinates = self._states_se2_array[in_interval, :2]
102
+ if len(coordinates) > 1:
103
+ return LineString(coordinates)
104
+
105
+ # fallback to slower method of shapely
106
+ return substring(self.linestring, start_distance, end_distance)
navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/route_utils.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Tuple
2
+
3
+ import numpy as np
4
+ from nuplan.common.actor_state.state_representation import StateSE2
5
+ from nuplan.common.maps.abstract_map import AbstractMap
6
+ from nuplan.common.maps.abstract_map_objects import RoadBlockGraphEdgeMapObject
7
+ from nuplan.common.maps.maps_datatypes import SemanticMapLayer
8
+ from nuplan.planning.simulation.occupancy_map.strtree_occupancy_map import STRTreeOccupancyMapFactory
9
+
10
+ from navsim.planning.simulation.planner.pdm_planner.utils.graph_search.bfs_roadblock import BreadthFirstSearchRoadBlock
11
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_geometry_utils import normalize_angle
12
+
13
+
14
+ def get_current_roadblock_candidates(
15
+ ego_pose: StateSE2,
16
+ map_api: AbstractMap,
17
+ route_roadblocks_dict: Dict[str, RoadBlockGraphEdgeMapObject],
18
+ heading_error_thresh: float = np.pi / 4,
19
+ displacement_error_thresh: float = 3,
20
+ ) -> Tuple[RoadBlockGraphEdgeMapObject, List[RoadBlockGraphEdgeMapObject]]:
21
+ """
22
+ Determines a set of roadblock candidate where ego is located
23
+ :param ego_pose: class containing ego position
24
+ :param map_api: map object
25
+ :param route_roadblocks_dict: dictionary of on-route roadblocks
26
+ :param heading_error_thresh: maximum heading error, defaults to np.pi/4
27
+ :param displacement_error_thresh: maximum displacement, defaults to 3
28
+ :return: tuple of most promising roadblock and other candidates
29
+ """
30
+ roadblock_candidates = []
31
+
32
+ layers = [SemanticMapLayer.ROADBLOCK, SemanticMapLayer.ROADBLOCK_CONNECTOR]
33
+ roadblock_dict = map_api.get_proximal_map_objects(point=ego_pose.point, radius=1.0, layers=layers)
34
+ roadblock_candidates = (
35
+ roadblock_dict[SemanticMapLayer.ROADBLOCK] + roadblock_dict[SemanticMapLayer.ROADBLOCK_CONNECTOR]
36
+ )
37
+
38
+ if not roadblock_candidates:
39
+ for layer in layers:
40
+ roadblock_id_, distance = map_api.get_distance_to_nearest_map_object(point=ego_pose.point, layer=layer)
41
+ roadblock = map_api.get_map_object(roadblock_id_, layer)
42
+
43
+ if roadblock:
44
+ roadblock_candidates.append(roadblock)
45
+
46
+ on_route_candidates, on_route_candidate_displacement_errors = [], []
47
+ candidates, candidate_displacement_errors = [], []
48
+
49
+ roadblock_displacement_errors = []
50
+ roadblock_heading_errors = []
51
+
52
+ for idx, roadblock in enumerate(roadblock_candidates):
53
+ lane_displacement_error, lane_heading_error = np.inf, np.inf
54
+
55
+ for lane in roadblock.interior_edges:
56
+ lane_discrete_path: List[StateSE2] = lane.baseline_path.discrete_path
57
+ lane_discrete_points = np.array([state.point.array for state in lane_discrete_path], dtype=np.float64)
58
+ lane_state_distances = ((lane_discrete_points - ego_pose.point.array[None, ...]) ** 2.0).sum(axis=-1) ** 0.5
59
+ argmin = np.argmin(lane_state_distances)
60
+
61
+ heading_error = np.abs(normalize_angle(lane_discrete_path[argmin].heading - ego_pose.heading))
62
+ displacement_error = lane_state_distances[argmin]
63
+
64
+ if displacement_error < lane_displacement_error:
65
+ lane_heading_error, lane_displacement_error = (
66
+ heading_error,
67
+ displacement_error,
68
+ )
69
+
70
+ if heading_error < heading_error_thresh and displacement_error < displacement_error_thresh:
71
+ if roadblock.id in route_roadblocks_dict.keys():
72
+ on_route_candidates.append(roadblock)
73
+ on_route_candidate_displacement_errors.append(displacement_error)
74
+ else:
75
+ candidates.append(roadblock)
76
+ candidate_displacement_errors.append(displacement_error)
77
+
78
+ roadblock_displacement_errors.append(lane_displacement_error)
79
+ roadblock_heading_errors.append(lane_heading_error)
80
+
81
+ if on_route_candidates: # prefer on-route roadblocks
82
+ return (
83
+ on_route_candidates[np.argmin(on_route_candidate_displacement_errors)],
84
+ on_route_candidates,
85
+ )
86
+ elif candidates: # fallback to most promising candidate
87
+ return candidates[np.argmin(candidate_displacement_errors)], candidates
88
+
89
+ # otherwise, just find any close roadblock
90
+ return (
91
+ roadblock_candidates[np.argmin(roadblock_displacement_errors)],
92
+ roadblock_candidates,
93
+ )
94
+
95
+
96
+ def route_roadblock_correction(
97
+ ego_pose: StateSE2,
98
+ map_api: AbstractMap,
99
+ route_roadblock_dict: Dict[str, RoadBlockGraphEdgeMapObject],
100
+ search_depth_backward: int = 15,
101
+ search_depth_forward: int = 30,
102
+ ) -> List[str]:
103
+ """
104
+ Applies several methods to correct route roadblocks.
105
+ :param ego_pose: class containing ego position
106
+ :param map_api: map object
107
+ :param route_roadblocks_dict: dictionary of on-route roadblocks
108
+ :param search_depth_backward: depth of forward BFS search, defaults to 15
109
+ :param search_depth_forward: depth of backward BFS search, defaults to 30
110
+ :return: list of roadblock id's of corrected route
111
+ """
112
+ # TODO: Refactor code for readability
113
+
114
+ starting_block, starting_block_candidates = get_current_roadblock_candidates(
115
+ ego_pose, map_api, route_roadblock_dict
116
+ )
117
+ starting_block_ids = [roadblock.id for roadblock in starting_block_candidates]
118
+
119
+ route_roadblocks = list(route_roadblock_dict.values())
120
+ route_roadblock_ids = list(route_roadblock_dict.keys())
121
+
122
+ # Fix 1: when agent starts off-route
123
+ if starting_block.id not in route_roadblock_ids:
124
+ # Backward search if current roadblock not in route
125
+ graph_search = BreadthFirstSearchRoadBlock(route_roadblock_ids[0], map_api, forward_search=False)
126
+ (path, path_id), path_found = graph_search.search(starting_block_ids, max_depth=search_depth_backward)
127
+
128
+ if path_found:
129
+ route_roadblocks[:0] = path[:-1]
130
+ route_roadblock_ids[:0] = path_id[:-1]
131
+
132
+ else:
133
+ # Forward search to any route roadblock
134
+ graph_search = BreadthFirstSearchRoadBlock(starting_block.id, map_api, forward_search=True)
135
+ (path, path_id), path_found = graph_search.search(route_roadblock_ids[:3], max_depth=search_depth_forward)
136
+
137
+ if path_found:
138
+ end_roadblock_idx = np.argmax(np.array(route_roadblock_ids) == path_id[-1])
139
+
140
+ route_roadblocks = route_roadblocks[end_roadblock_idx + 1 :]
141
+ route_roadblock_ids = route_roadblock_ids[end_roadblock_idx + 1 :]
142
+
143
+ route_roadblocks[:0] = path
144
+ route_roadblock_ids[:0] = path_id
145
+
146
+ # Fix 2: check if roadblocks are linked, search for links if not
147
+ roadblocks_to_append = {}
148
+ for i in range(len(route_roadblocks) - 1):
149
+ next_incoming_block_ids = [_roadblock.id for _roadblock in route_roadblocks[i + 1].incoming_edges]
150
+ is_incoming = route_roadblock_ids[i] in next_incoming_block_ids
151
+
152
+ if is_incoming:
153
+ continue
154
+
155
+ graph_search = BreadthFirstSearchRoadBlock(route_roadblock_ids[i], map_api, forward_search=True)
156
+ (path, path_id), path_found = graph_search.search(route_roadblock_ids[i + 1], max_depth=search_depth_forward)
157
+
158
+ if path_found and path and len(path) >= 3:
159
+ path, path_id = path[1:-1], path_id[1:-1]
160
+ roadblocks_to_append[i] = (path, path_id)
161
+
162
+ # append missing intermediate roadblocks
163
+ offset = 1
164
+ for i, (path, path_id) in roadblocks_to_append.items():
165
+ route_roadblocks[i + offset : i + offset] = path
166
+ route_roadblock_ids[i + offset : i + offset] = path_id
167
+ offset += len(path)
168
+
169
+ # Fix 3: cut route-loops
170
+ route_roadblocks, route_roadblock_ids = remove_route_loops(route_roadblocks, route_roadblock_ids)
171
+
172
+ return route_roadblock_ids
173
+
174
+
175
+ def remove_route_loops(
176
+ route_roadblocks: List[RoadBlockGraphEdgeMapObject],
177
+ route_roadblock_ids: List[str],
178
+ ) -> Tuple[List[str], List[RoadBlockGraphEdgeMapObject]]:
179
+ """
180
+ Remove ending of route, if the roadblock are intersecting the route (forming a loop).
181
+ :param route_roadblocks: input route roadblocks
182
+ :param route_roadblock_ids: input route roadblocks ids
183
+ :return: tuple of ids and roadblocks of route without loops
184
+ """
185
+
186
+ roadblock_occupancy_map = None
187
+ loop_idx = None
188
+
189
+ for idx, roadblock in enumerate(route_roadblocks):
190
+ # loops only occur at intersection, thus searching for roadblock-connectors.
191
+ if str(roadblock.__class__.__name__) == "NuPlanRoadBlockConnector":
192
+ if not roadblock_occupancy_map:
193
+ roadblock_occupancy_map = STRTreeOccupancyMapFactory.get_from_geometry(
194
+ [roadblock.polygon], [roadblock.id]
195
+ )
196
+ continue
197
+
198
+ strtree, index_by_id = roadblock_occupancy_map._build_strtree()
199
+ indices = strtree.query(roadblock.polygon)
200
+ if len(indices) > 0:
201
+ for geom in strtree.geometries.take(indices):
202
+ area = geom.intersection(roadblock.polygon).area
203
+ if area > 1:
204
+ loop_idx = idx
205
+ break
206
+ if loop_idx:
207
+ break
208
+
209
+ roadblock_occupancy_map.insert(roadblock.id, roadblock.polygon)
210
+
211
+ if loop_idx:
212
+ route_roadblocks = route_roadblocks[:loop_idx]
213
+ route_roadblock_ids = route_roadblock_ids[:loop_idx]
214
+
215
+ return route_roadblocks, route_roadblock_ids
navsim_v2/navsim/planning/training/__init__.py ADDED
File without changes
navsim_v2/navsim/planning/training/abstract_feature_target_builder.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import abstractmethod
2
+ from typing import Dict
3
+
4
+ from torch import Tensor
5
+
6
+ from navsim.common.dataclasses import AgentInput, Scene
7
+
8
+
9
+ class AbstractFeatureBuilder:
10
+ """Abstract class of feature builder for agent training."""
11
+
12
+ def __init__(self):
13
+ pass
14
+
15
+ @abstractmethod
16
+ def get_unique_name(self) -> str:
17
+ """
18
+ :return: Unique name of created feature.
19
+ """
20
+
21
+ @abstractmethod
22
+ def compute_features(self, agent_input: AgentInput) -> Dict[str, Tensor]:
23
+ """
24
+ Computes features from the AgentInput object, i.e., without access to ground-truth.
25
+ Outputs a dictionary where each item has a unique identifier and maps to a single feature tensor.
26
+ One FeatureBuilder can return a dict with multiple FeatureTensors.
27
+ """
28
+
29
+
30
+ class AbstractTargetBuilder:
31
+ def __init__(self):
32
+ pass
33
+
34
+ @abstractmethod
35
+ def get_unique_name(self) -> str:
36
+ """
37
+ :return: Unique name of created target.
38
+ """
39
+
40
+ @abstractmethod
41
+ def compute_targets(self, scene: Scene) -> Dict[str, Tensor]:
42
+ """
43
+ Computes targets from the Scene object, i.e., with access to ground-truth.
44
+ Outputs a dictionary where each item has a unique identifier and maps to a single target tensor.
45
+ One TargetBuilder can return a dict with multiple TargetTensors.
46
+ """
navsim_v2/navsim/planning/training/agent_lightning_module.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Tuple
2
+
3
+ import pytorch_lightning as pl
4
+ import torch
5
+ from torch import Tensor
6
+
7
+ from navsim.agents.abstract_agent import AbstractAgent
8
+ from navsim.agents.drive_jepa_perception_based.score_module.epdms import (
9
+ compose_epdms,
10
+ )
11
+
12
+
13
+ class AgentLightningModule(pl.LightningModule):
14
+ """Pytorch lightning wrapper for learnable agent."""
15
+
16
+ def __init__(self, agent: AbstractAgent):
17
+ """
18
+ Initialise the lightning module wrapper.
19
+ :param agent: agent interface in NAVSIM
20
+ """
21
+ super().__init__()
22
+ self.agent = agent
23
+
24
+ def _step(self, batch: Tuple[Dict[str, Tensor], Dict[str, Tensor]], logging_prefix: str) -> Tensor:
25
+ """
26
+ Propagates the model forward and backwards and computes/logs losses and metrics.
27
+ :param batch: tuple of dictionaries for feature and target tensors (batched)
28
+ :param logging_prefix: prefix where to log step
29
+ :return: scalar loss
30
+ """
31
+ features, targets = batch
32
+ prediction = self.agent.forward(features)
33
+ loss = self.agent.compute_loss(features, targets, prediction)
34
+ if isinstance(loss, dict):
35
+ for key, value in loss.items():
36
+ self.log(
37
+ f"{logging_prefix}/{key}",
38
+ value,
39
+ on_step=True,
40
+ on_epoch=False,
41
+ prog_bar=key in {"loss", "score", "best_score"},
42
+ sync_dist=True,
43
+ )
44
+ return loss["loss"]
45
+ self.log(
46
+ f"{logging_prefix}/loss",
47
+ loss,
48
+ on_step=True,
49
+ on_epoch=True,
50
+ prog_bar=True,
51
+ sync_dist=True,
52
+ )
53
+ return loss
54
+
55
+ def training_step(self, batch: Tuple[Dict[str, Tensor], Dict[str, Tensor]], batch_idx: int) -> Tensor:
56
+ """
57
+ Step called on training samples
58
+ :param batch: tuple of dictionaries for feature and target tensors (batched)
59
+ :param batch_idx: index of batch (ignored)
60
+ :return: scalar loss
61
+ """
62
+ return self._step(batch, "train")
63
+
64
+ def validation_step(self, batch: Tuple[Dict[str, Tensor], Dict[str, Tensor]], batch_idx: int):
65
+ """
66
+ Step called on validation samples
67
+ :param batch: tuple of dictionaries for feature and target tensors (batched)
68
+ :param batch_idx: index of batch (ignored)
69
+ :return: scalar loss
70
+ """
71
+ if "perception_based" not in self.agent.name().lower():
72
+ return self._step(batch, "val")
73
+
74
+ features, targets = batch
75
+ predictions = self.agent.forward(features)
76
+ selected = predictions["trajectory"][:, None]
77
+ (
78
+ final_score,
79
+ best_score,
80
+ proposal_scores,
81
+ l2,
82
+ trajectory_scores,
83
+ ) = self.agent.compute_score(targets, selected)
84
+
85
+ selected_indices = torch.argmax(predictions["pdm_score"], dim=1)
86
+ selected_metrics = predictions["pred_metric_values"][
87
+ torch.arange(len(selected_indices), device=selected_indices.device),
88
+ selected_indices,
89
+ ]
90
+ predicted_stage1 = compose_epdms(
91
+ selected_metrics, include_extended_comfort=False
92
+ )
93
+ score_error = torch.abs(
94
+ predicted_stage1 - proposal_scores[:, 0]
95
+ ).mean()
96
+ mean_score = proposal_scores.mean()
97
+
98
+ scalar_metrics = {
99
+ "score": final_score,
100
+ "best_score": best_score,
101
+ "mean_score": mean_score,
102
+ "score_error": score_error,
103
+ "l2": l2,
104
+ "collision": trajectory_scores[:, 0].mean(),
105
+ "dac": trajectory_scores[:, 1].mean(),
106
+ "tlc": trajectory_scores[:, 2].mean(),
107
+ "ddc": trajectory_scores[:, 3].mean(),
108
+ "progress": trajectory_scores[:, 4].mean(),
109
+ "ttc": trajectory_scores[:, 5].mean(),
110
+ "lk": trajectory_scores[:, 6].mean(),
111
+ "comfort": trajectory_scores[:, 7].mean(),
112
+ }
113
+ for name, value in scalar_metrics.items():
114
+ self.log(
115
+ f"val/{name}",
116
+ value,
117
+ on_step=name == "score",
118
+ on_epoch=True,
119
+ prog_bar=name in {"score", "best_score", "score_error"},
120
+ sync_dist=True,
121
+ )
122
+ return final_score
123
+
124
+ def configure_optimizers(self):
125
+ """Inherited, see superclass."""
126
+ return self.agent.get_optimizers()
navsim_v2/navsim/planning/training/callbacks/time_logging_callback.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from typing import Any, Optional
3
+
4
+ import pytorch_lightning as pl
5
+
6
+
7
+ class TimeLoggingCallback(pl.Callback):
8
+ """Simple lightning callback to log training time."""
9
+
10
+ def __init__(self) -> None:
11
+ pass
12
+
13
+ def on_validation_epoch_start(self, trainer: pl.Trainer, lightning_module: pl.LightningModule) -> None:
14
+ """Inherited, see superclass."""
15
+ self.val_start = time.time()
16
+
17
+ def on_validation_epoch_end(self, trainer: pl.Trainer, lightning_module: pl.LightningModule) -> None:
18
+ """Inherited, see superclass."""
19
+ lightning_module.log_dict(
20
+ {
21
+ "time_eval": time.time() - self.val_start,
22
+ "step": lightning_module.current_epoch,
23
+ }
24
+ )
25
+
26
+ def on_test_epoch_start(self, trainer: pl.Trainer, lightning_module: pl.LightningModule) -> None:
27
+ """Inherited, see superclass."""
28
+ self.test_start = time.time()
29
+
30
+ def on_test_epoch_end(self, trainer: pl.Trainer, lightning_module: pl.LightningModule) -> None:
31
+ """Inherited, see superclass."""
32
+ lightning_module.log_dict(
33
+ {
34
+ "time_test": time.time() - self.test_start,
35
+ "step": lightning_module.current_epoch,
36
+ }
37
+ )
38
+
39
+ def on_train_epoch_start(self, trainer: pl.Trainer, lightning_module: pl.LightningModule) -> None:
40
+ """Inherited, see superclass."""
41
+ self.train_start = time.time()
42
+
43
+ def on_train_epoch_end(
44
+ self, trainer: pl.Trainer, lightning_module: pl.LightningModule, unused: Optional[Any] = None
45
+ ) -> None:
46
+ """Inherited, see superclass."""
47
+ lightning_module.log_dict(
48
+ {
49
+ "time_epoch": time.time() - self.train_start,
50
+ "step": lightning_module.current_epoch,
51
+ }
52
+ )
navsim_v2/navsim/planning/training/dataset.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gzip
2
+ import logging
3
+ import os
4
+ import pickle
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ import torch
9
+ from tqdm import tqdm
10
+
11
+ from navsim.common.dataloader import SceneLoader
12
+ from navsim.planning.training.abstract_feature_target_builder import AbstractFeatureBuilder, AbstractTargetBuilder
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def load_feature_target_from_pickle(path: Path) -> Dict[str, torch.Tensor]:
18
+ """Helper function to load pickled feature/target from path."""
19
+ with gzip.open(path, "rb") as f:
20
+ data_dict: Dict[str, torch.Tensor] = pickle.load(f)
21
+ return data_dict
22
+
23
+
24
+ def dump_feature_target_to_pickle(path: Path, data_dict: Dict[str, torch.Tensor]) -> None:
25
+ """Helper function to save feature/target to pickle."""
26
+ # Use compresslevel = 1 to compress the size but also has fast write and read.
27
+ with gzip.open(path, "wb", compresslevel=1) as f:
28
+ pickle.dump(data_dict, f)
29
+
30
+
31
+ class CacheOnlyDataset(torch.utils.data.Dataset):
32
+ """Dataset wrapper for feature/target datasets from cache only."""
33
+
34
+ def __init__(
35
+ self,
36
+ cache_path: str,
37
+ feature_builders: List[AbstractFeatureBuilder],
38
+ target_builders: List[AbstractTargetBuilder],
39
+ log_names: Optional[List[str]] = None,
40
+ ):
41
+ """
42
+ Initializes the dataset module.
43
+ :param cache_path: directory to cache folder
44
+ :param feature_builders: list of feature builders
45
+ :param target_builders: list of target builders
46
+ :param log_names: optional list of log folder to consider, defaults to None
47
+ """
48
+ super().__init__()
49
+ assert Path(cache_path).is_dir(), f"Cache path {cache_path} does not exist!"
50
+ self._cache_path = Path(cache_path)
51
+
52
+ if log_names is not None:
53
+ self.log_names = [Path(log_name) for log_name in log_names if (self._cache_path / log_name).is_dir()]
54
+ else:
55
+ self.log_names = [log_name for log_name in self._cache_path.iterdir()]
56
+
57
+ self._feature_builders = feature_builders
58
+ self._target_builders = target_builders
59
+ self._valid_cache_paths: Dict[str, Path] = self._load_valid_caches(
60
+ cache_path=self._cache_path,
61
+ feature_builders=self._feature_builders,
62
+ target_builders=self._target_builders,
63
+ log_names=self.log_names,
64
+ )
65
+ self.tokens = list(self._valid_cache_paths.keys())
66
+
67
+ def __len__(self) -> int:
68
+ """
69
+ :return: number of samples to load
70
+ """
71
+ return len(self.tokens)
72
+
73
+ def __getitem__(self, idx: int) -> Tuple[Dict[str, torch.Tensor], Dict[str, torch.Tensor]]:
74
+ """
75
+ Loads and returns pair of feature and target dict from data.
76
+ :param idx: index of sample to load.
77
+ :return: tuple of feature and target dictionary
78
+ """
79
+ return self._load_scene_with_token(self.tokens[idx])
80
+
81
+ @staticmethod
82
+ def _load_valid_caches(
83
+ cache_path: Path,
84
+ feature_builders: List[AbstractFeatureBuilder],
85
+ target_builders: List[AbstractTargetBuilder],
86
+ log_names: List[Path],
87
+ ) -> Dict[str, Path]:
88
+ """
89
+ Helper method to load valid cache paths.
90
+ :param cache_path: directory of training cache folder
91
+ :param feature_builders: list of feature builders
92
+ :param target_builders: list of target builders
93
+ :param log_names: list of log paths to load
94
+ :return: dictionary of tokens and sample paths as keys / values
95
+ """
96
+
97
+ valid_cache_paths: Dict[str, Path] = {}
98
+
99
+ for log_name in tqdm(log_names, desc="Loading Valid Caches"):
100
+ log_path = cache_path / log_name
101
+ for token_path in log_path.iterdir():
102
+ found_caches: List[bool] = []
103
+ for builder in feature_builders + target_builders:
104
+ data_dict_path = token_path / (builder.get_unique_name() + ".gz")
105
+ found_caches.append(data_dict_path.is_file())
106
+ if all(found_caches):
107
+ valid_cache_paths[token_path.name] = token_path
108
+
109
+ return valid_cache_paths
110
+
111
+ def _load_scene_with_token(self, token: str) -> Tuple[Dict[str, torch.Tensor], Dict[str, torch.Tensor]]:
112
+ """
113
+ Helper method to load sample tensors given token
114
+ :param token: unique string identifier of sample
115
+ :return: tuple of feature and target dictionaries
116
+ """
117
+
118
+ token_path = self._valid_cache_paths[token]
119
+
120
+ features: Dict[str, torch.Tensor] = {}
121
+ for builder in self._feature_builders:
122
+ data_dict_path = token_path / (builder.get_unique_name() + ".gz")
123
+ data_dict = load_feature_target_from_pickle(data_dict_path)
124
+ features.update(data_dict)
125
+
126
+ targets: Dict[str, torch.Tensor] = {}
127
+ for builder in self._target_builders:
128
+ data_dict_path = token_path / (builder.get_unique_name() + ".gz")
129
+ data_dict = load_feature_target_from_pickle(data_dict_path)
130
+ targets.update(data_dict)
131
+
132
+ return (features, targets)
133
+
134
+
135
+ class Dataset(torch.utils.data.Dataset):
136
+ def __init__(
137
+ self,
138
+ scene_loader: SceneLoader,
139
+ feature_builders: List[AbstractFeatureBuilder],
140
+ target_builders: List[AbstractTargetBuilder],
141
+ cache_path: Optional[str] = None,
142
+ force_cache_computation: bool = False,
143
+ ):
144
+ super().__init__()
145
+ self._scene_loader = scene_loader
146
+ self._feature_builders = feature_builders
147
+ self._target_builders = target_builders
148
+
149
+ self._cache_path: Optional[Path] = Path(cache_path) if cache_path else None
150
+ self._force_cache_computation = force_cache_computation
151
+ self._valid_cache_paths: Dict[str, Path] = self._load_valid_caches(
152
+ self._cache_path, feature_builders, target_builders
153
+ )
154
+
155
+ if self._cache_path is not None:
156
+ self.cache_dataset()
157
+
158
+ @staticmethod
159
+ def _load_valid_caches(
160
+ cache_path: Optional[Path],
161
+ feature_builders: List[AbstractFeatureBuilder],
162
+ target_builders: List[AbstractTargetBuilder],
163
+ ) -> Dict[str, Path]:
164
+ """
165
+ Helper method to load valid cache paths.
166
+ :param cache_path: directory of training cache folder
167
+ :param feature_builders: list of feature builders
168
+ :param target_builders: list of target builders
169
+ :return: dictionary of tokens and sample paths as keys / values
170
+ """
171
+
172
+ valid_cache_paths: Dict[str, Path] = {}
173
+
174
+ if (cache_path is not None) and cache_path.is_dir():
175
+ for log_path in cache_path.iterdir():
176
+ for token_path in log_path.iterdir():
177
+ found_caches: List[bool] = []
178
+ for builder in feature_builders + target_builders:
179
+ data_dict_path = token_path / (builder.get_unique_name() + ".gz")
180
+ found_caches.append(data_dict_path.is_file())
181
+ if all(found_caches):
182
+ valid_cache_paths[token_path.name] = token_path
183
+
184
+ return valid_cache_paths
185
+
186
+ def _cache_scene_with_token(self, token: str) -> None:
187
+ """
188
+ Helper function to compute feature / targets and save in cache.
189
+ :param token: unique identifier of scene to cache
190
+ """
191
+
192
+ scene = self._scene_loader.get_scene_from_token(token)
193
+ agent_input = scene.get_agent_input()
194
+
195
+ metadata = scene.scene_metadata
196
+ token_path = self._cache_path / metadata.log_name / metadata.initial_token
197
+ os.makedirs(token_path, exist_ok=True)
198
+
199
+ for builder in self._feature_builders:
200
+ data_dict_path = token_path / (builder.get_unique_name() + ".gz")
201
+ data_dict = builder.compute_features(agent_input)
202
+ dump_feature_target_to_pickle(data_dict_path, data_dict)
203
+
204
+ for builder in self._target_builders:
205
+ data_dict_path = token_path / (builder.get_unique_name() + ".gz")
206
+ data_dict = builder.compute_targets(scene)
207
+ dump_feature_target_to_pickle(data_dict_path, data_dict)
208
+
209
+ self._valid_cache_paths[token] = token_path
210
+
211
+ def _load_scene_with_token(self, token: str) -> Tuple[Dict[str, torch.Tensor], Dict[str, torch.Tensor]]:
212
+ """
213
+ Helper function to load feature / targets from cache.
214
+ :param token: unique identifier of scene to load
215
+ :return: tuple of feature and target dictionaries
216
+ """
217
+
218
+ token_path = self._valid_cache_paths[token]
219
+
220
+ features: Dict[str, torch.Tensor] = {}
221
+ for builder in self._feature_builders:
222
+ data_dict_path = token_path / (builder.get_unique_name() + ".gz")
223
+ data_dict = load_feature_target_from_pickle(data_dict_path)
224
+ features.update(data_dict)
225
+
226
+ targets: Dict[str, torch.Tensor] = {}
227
+ for builder in self._target_builders:
228
+ data_dict_path = token_path / (builder.get_unique_name() + ".gz")
229
+ data_dict = load_feature_target_from_pickle(data_dict_path)
230
+ targets.update(data_dict)
231
+
232
+ return (features, targets)
233
+
234
+ def cache_dataset(self) -> None:
235
+ """Caches complete dataset into cache folder."""
236
+
237
+ assert self._cache_path is not None, "Dataset did not receive a cache path!"
238
+ os.makedirs(self._cache_path, exist_ok=True)
239
+
240
+ # determine tokens to cache
241
+ if self._force_cache_computation:
242
+ tokens_to_cache = self._scene_loader.tokens
243
+ else:
244
+ tokens_to_cache = set(self._scene_loader.tokens) - set(self._valid_cache_paths.keys())
245
+ tokens_to_cache = list(tokens_to_cache)
246
+ logger.info(
247
+ f"""
248
+ Starting caching of {len(tokens_to_cache)} tokens.
249
+ Note: Caching tokens within the training loader is slow. Only use it with a small number of tokens.
250
+ You can cache large numbers of tokens using the `run_dataset_caching.py` python script.
251
+ """
252
+ )
253
+
254
+ for token in tqdm(tokens_to_cache, desc="Caching Dataset"):
255
+ self._cache_scene_with_token(token)
256
+
257
+ def __len__(self) -> None:
258
+ """
259
+ :return: number of samples to load
260
+ """
261
+ return len(self._scene_loader)
262
+
263
+ def __getitem__(self, idx: int) -> Tuple[Dict[str, torch.Tensor], Dict[str, torch.Tensor]]:
264
+ """
265
+ Get features or targets either from cache or computed on-the-fly.
266
+ :param idx: index of sample to load.
267
+ :return: tuple of feature and target dictionary
268
+ """
269
+
270
+ token = self._scene_loader.tokens[idx]
271
+ features: Dict[str, torch.Tensor] = {}
272
+ targets: Dict[str, torch.Tensor] = {}
273
+
274
+ if self._cache_path is not None:
275
+ assert (
276
+ token in self._valid_cache_paths.keys()
277
+ ), f"The token {token} has not been cached yet, please call cache_dataset first!"
278
+
279
+ features, targets = self._load_scene_with_token(token)
280
+ else:
281
+ scene = self._scene_loader.get_scene_from_token(self._scene_loader.tokens[idx])
282
+ agent_input = scene.get_agent_input()
283
+ for builder in self._feature_builders:
284
+ features.update(builder.compute_features(agent_input))
285
+ for builder in self._target_builders:
286
+ targets.update(builder.compute_targets(scene))
287
+
288
+ return (features, targets)
navsim_v2/navsim/planning/utils/multithreading/__init__.py ADDED
File without changes
navsim_v2/navsim/planning/utils/multithreading/worker_ray_no_torch.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ from concurrent.futures import Future
4
+ from pathlib import Path
5
+ from typing import Any, Iterable, List, Optional, Union
6
+
7
+ import ray
8
+ from nuplan.planning.utils.multithreading.ray_execution import ray_map
9
+ from nuplan.planning.utils.multithreading.worker_pool import Task, WorkerPool, WorkerResources
10
+ from psutil import cpu_count
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Silent botocore which is polluting the terminal because of serialization and deserialization
15
+ # with following message: INFO:botocore.credentials:Credentials found in config file: ~/.aws/config
16
+ logging.getLogger("botocore").setLevel(logging.WARNING)
17
+
18
+
19
+ def initialize_ray(
20
+ master_node_ip: Optional[str] = None,
21
+ threads_per_node: Optional[int] = None,
22
+ local_mode: bool = False,
23
+ log_to_driver: bool = True,
24
+ use_distributed: bool = False,
25
+ ) -> WorkerResources:
26
+ """
27
+ Initialize ray worker.
28
+ ENV_VAR_MASTER_NODE_IP="master node IP".
29
+ ENV_VAR_MASTER_NODE_PASSWORD="password to the master node".
30
+ ENV_VAR_NUM_NODES="number of nodes available".
31
+ :param master_node_ip: if available, ray will connect to remote cluster.
32
+ :param threads_per_node: Number of threads to use per node.
33
+ :param log_to_driver: If true, the output from all of the worker
34
+ processes on all nodes will be directed to the driver.
35
+ :param local_mode: If true, the code will be executed serially. This
36
+ is useful for debugging.
37
+ :param use_distributed: If true, and the env vars are available,
38
+ ray will launch in distributed mode
39
+ :return: created WorkerResources.
40
+ """
41
+ # Env variables which are set through SLURM script
42
+ env_var_master_node_ip = "ip_head"
43
+ env_var_master_node_password = "redis_password"
44
+ env_var_num_nodes = "num_nodes"
45
+
46
+ # Read number of CPU cores on current machine
47
+ number_of_cpus_per_node = threads_per_node if threads_per_node else cpu_count(logical=True)
48
+ number_of_gpus_per_node = 0 # no cuda support
49
+ if not number_of_gpus_per_node:
50
+ logger.info("Not using GPU in ray")
51
+
52
+ # Find a way in how the ray should be initialized
53
+ if master_node_ip and use_distributed:
54
+ # Connect to ray remotely to node ip
55
+ logger.info(f"Connecting to cluster at: {master_node_ip}!")
56
+ ray.init(address=f"ray://{master_node_ip}:10001", local_mode=local_mode, log_to_driver=log_to_driver)
57
+ number_of_nodes = 1
58
+ elif env_var_master_node_ip in os.environ and use_distributed:
59
+ # In this way, we started ray on the current machine which generated password and master node ip:
60
+ # It was started with "ray start --head"
61
+ number_of_nodes = int(os.environ[env_var_num_nodes])
62
+ master_node_ip = os.environ[env_var_master_node_ip].split(":")[0]
63
+ redis_password = os.environ[env_var_master_node_password].split(":")[0]
64
+ logger.info(f"Connecting as part of a cluster at: {master_node_ip} with password: {redis_password}!")
65
+ # Connect to cluster, follow to https://docs.ray.io/en/latest/package-ref.html for more info
66
+ ray.init(
67
+ address="auto",
68
+ _node_ip_address=master_node_ip,
69
+ _redis_password=redis_password,
70
+ log_to_driver=log_to_driver,
71
+ local_mode=local_mode,
72
+ )
73
+ else:
74
+ # In this case, we will just start ray directly from this script
75
+ number_of_nodes = 1
76
+ logger.info("Starting ray local!")
77
+ ray.init(
78
+ num_cpus=number_of_cpus_per_node,
79
+ dashboard_host="0.0.0.0",
80
+ local_mode=local_mode,
81
+ log_to_driver=log_to_driver,
82
+ )
83
+
84
+ return WorkerResources(
85
+ number_of_nodes=number_of_nodes,
86
+ number_of_cpus_per_node=number_of_cpus_per_node,
87
+ number_of_gpus_per_node=number_of_gpus_per_node,
88
+ )
89
+
90
+
91
+ class RayDistributedNoTorch(WorkerPool):
92
+ """
93
+ This worker uses ray to distribute work across all available threads.
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ master_node_ip: Optional[str] = None,
99
+ threads_per_node: Optional[int] = None,
100
+ debug_mode: bool = False,
101
+ log_to_driver: bool = True,
102
+ output_dir: Optional[Union[str, Path]] = None,
103
+ logs_subdir: Optional[str] = "logs",
104
+ use_distributed: bool = False,
105
+ ):
106
+ """
107
+ Initialize ray worker.
108
+ :param master_node_ip: if available, ray will connect to remote cluster.
109
+ :param threads_per_node: Number of threads to use per node.
110
+ :param debug_mode: If true, the code will be executed serially. This
111
+ is useful for debugging.
112
+ :param log_to_driver: If true, the output from all of the worker
113
+ processes on all nodes will be directed to the driver.
114
+ :param output_dir: Experiment output directory.
115
+ :param logs_subdir: Subdirectory inside experiment dir to store worker logs.
116
+ :param use_distributed: Boolean flag to explicitly enable/disable distributed computation
117
+ """
118
+ self._master_node_ip = master_node_ip
119
+ self._threads_per_node = threads_per_node
120
+ self._local_mode = debug_mode
121
+ self._log_to_driver = log_to_driver
122
+ self._log_dir: Optional[Path] = Path(output_dir) / (logs_subdir or "") if output_dir is not None else None
123
+ self._use_distributed = use_distributed
124
+ super().__init__(self.initialize())
125
+
126
+ def initialize(self) -> WorkerResources:
127
+ """
128
+ Initialize ray.
129
+ :return: created WorkerResources.
130
+ """
131
+ # In case ray was already running, shut it down. This occurs mainly in tests
132
+ if ray.is_initialized():
133
+ logger.warning("Ray is running, we will shut it down before starting again!")
134
+ ray.shutdown()
135
+
136
+ return initialize_ray(
137
+ master_node_ip=self._master_node_ip,
138
+ threads_per_node=self._threads_per_node,
139
+ local_mode=self._local_mode,
140
+ log_to_driver=self._log_to_driver,
141
+ use_distributed=self._use_distributed,
142
+ )
143
+
144
+ def shutdown(self) -> None:
145
+ """
146
+ Shutdown the worker and clear memory.
147
+ """
148
+ ray.shutdown()
149
+
150
+ def _map(self, task: Task, *item_lists: Iterable[List[Any]], verbose: bool = False) -> List[Any]:
151
+ """Inherited, see superclass."""
152
+ del verbose
153
+ return ray_map(task, *item_lists, log_dir=self._log_dir) # type: ignore
154
+
155
+ def submit(self, task: Task, *args: Any, **kwargs: Any) -> Future[Any]:
156
+ """Inherited, see superclass."""
157
+ remote_fn = ray.remote(task.fn).options(num_gpus=task.num_gpus, num_cpus=task.num_cpus)
158
+ object_ids: ray._raylet.ObjectRef = remote_fn.remote(*args, **kwargs)
159
+ return object_ids.future() # type: ignore
navsim_v2/navsim/traffic_agents_policies/__init__.py ADDED
File without changes
navsim_v2/navsim/traffic_agents_policies/abstract_traffic_agents_policy.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Tuple
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ from nuplan.common.actor_state.agent import Agent
7
+ from nuplan.common.actor_state.tracked_objects import TrackedObjects
8
+ from nuplan.common.actor_state.tracked_objects_types import TrackedObjectType
9
+ from nuplan.planning.simulation.observation.observation_type import DetectionsTracks
10
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
11
+ from nuplan.planning.training.preprocessing.utils.agents_preprocessing import (
12
+ extract_and_pad_agent_states,
13
+ filter_agents,
14
+ )
15
+
16
+ from navsim.planning.metric_caching.metric_cache import MetricCache
17
+
18
+
19
+ def extract_vehicle_trajectories_from_detections_tracks(
20
+ detections_tracks: List[DetectionsTracks],
21
+ reverse_padding: bool = False,
22
+ ) -> Tuple[List[npt.NDArray[np.float64]], List[npt.NDArray[np.bool_]], List[str]]:
23
+ """
24
+ Extract the agent states as an array and pads it with the most recent available states.
25
+ Note: agents that don't appear in the current time step are ignored.
26
+ See nuplan's extract_and_pad_agent_states for details
27
+ :param detections_tracks: list of DetectionsTracks objects
28
+ :param reverse_padding: if True, the last element in the list will be used as the filter.
29
+ Set to true when filtering past trajectories
30
+ :returns
31
+ agent_trajectories: list of length num agents of agent trajectories of shape (num timesteps, 6)
32
+ agent_trajectory_masks: list of length num agents of agent trajectory masks of shape (num timesteps, 1)
33
+ agent_tokens: list of length num agents of agent tokens
34
+ """
35
+
36
+ def _state_extractor(scene_object: Agent) -> npt.NDArray[np.float64]:
37
+ return np.array(
38
+ [
39
+ scene_object.center.x,
40
+ scene_object.center.y,
41
+ scene_object.center.heading,
42
+ scene_object.velocity.x,
43
+ scene_object.velocity.y,
44
+ 0.0, # acceleration.x,
45
+ 0.0, # acceleration.y,
46
+ 0.0, # steering angle
47
+ 0.0, # steering rate
48
+ 0.0, # angular_velocity,
49
+ 0.0, # angular_acceleration,
50
+ ]
51
+ )
52
+
53
+ def _token_extractor(scene_object: Agent) -> str:
54
+ return scene_object.track_token
55
+
56
+ vehicle_tracks = filter_tracked_objects_by_type(detections_tracks, TrackedObjectType.VEHICLE)
57
+ tracked_objects: List[TrackedObjects] = [t.tracked_objects for t in vehicle_tracks]
58
+ filtered_agents = filter_agents(tracked_objects, reverse=reverse_padding)
59
+
60
+ # agent_states and masks are nested lists of [num_frames, num_agents]
61
+ (agent_states_horizon, agent_states_horizon_masks,) = extract_and_pad_agent_states(
62
+ agent_trajectories=filtered_agents,
63
+ state_extractor=_state_extractor,
64
+ reverse=reverse_padding,
65
+ )
66
+ agent_trajectories = [
67
+ np.array([agent_states_horizon[t][a] for t in range(len(agent_states_horizon))])
68
+ for a in range(len(agent_states_horizon[0]))
69
+ ]
70
+ agent_trajectory_masks = [
71
+ np.array([agent_states_horizon_masks[t][a] for t in range(len(agent_states_horizon_masks))])
72
+ for a in range(len(agent_states_horizon_masks[0]))
73
+ ]
74
+
75
+ # agent_states and masks are nested lists of [num_frames, num_agents]
76
+ (agent_tokens_horizon, _,) = extract_and_pad_agent_states(
77
+ agent_trajectories=filtered_agents,
78
+ state_extractor=_token_extractor,
79
+ reverse=reverse_padding,
80
+ )
81
+ # tokens remain constant over time so we only need the first frame
82
+ agent_tokens = agent_tokens_horizon[0]
83
+ return agent_trajectories, agent_trajectory_masks, agent_tokens
84
+
85
+
86
+ def filter_tracked_objects_by_type(
87
+ tracked_objects: List[DetectionsTracks], object_type: TrackedObjectType
88
+ ) -> List[DetectionsTracks]:
89
+ return [
90
+ DetectionsTracks(TrackedObjects(p.tracked_objects.get_tracked_objects_of_type(object_type)))
91
+ for p in tracked_objects
92
+ ]
93
+
94
+
95
+ def filter_tracked_objects_by_types(
96
+ tracked_objects: List[DetectionsTracks], object_types: List[TrackedObjectType]
97
+ ) -> List[DetectionsTracks]:
98
+ return [
99
+ DetectionsTracks(TrackedObjects(p.tracked_objects.get_tracked_objects_of_types(object_types)))
100
+ for p in tracked_objects
101
+ ]
102
+
103
+
104
+ class AbstractTrafficAgentsPolicy(ABC):
105
+ """Interface for background traffic agents in NAVSIM."""
106
+
107
+ @abstractmethod
108
+ def __init__(self, future_trajectory_sampling: TrajectorySampling) -> None:
109
+ pass
110
+
111
+ @abstractmethod
112
+ def get_list_of_simulated_object_types(self) -> List[TrackedObjectType]:
113
+ """
114
+ Returns the list of object types that the policy simulates.
115
+ For all remaining objects, the ground truth future tracks are used.
116
+ The policy should only return the tracks for the object types it simulates.
117
+ The remaining objects are automatically merged to the DetectionsTracks.
118
+ """
119
+
120
+ def simulate_environment(
121
+ self, simulated_ego_states: npt.NDArray[np.float64], metric_cache: MetricCache
122
+ ) -> List[DetectionsTracks]:
123
+ """
124
+ Simulates the environment, including the ego agent and traffic agents.
125
+ :param simulated_ego_states: trajectory the ego-vehicle will follow
126
+ :param metric_cache: general metric cache with describing the state of all agents and their environment
127
+ :return: DetectionsTracks object containing the simulated traffic agents
128
+ """
129
+
130
+ simulated_detections_tracks = self.simulate_traffic_agents(simulated_ego_states, metric_cache)
131
+
132
+ # assert that the simulated detectionstracks only include the object types that the policy simulates
133
+ assert all(
134
+ all(
135
+ obj.tracked_object_type in self.get_list_of_simulated_object_types()
136
+ for obj in detections_tracks.tracked_objects
137
+ )
138
+ for detections_tracks in simulated_detections_tracks
139
+ ), "Traffic agents policy must only return detections tracks of the object types it simulates (see get_list_of_simulated_object_types)"
140
+
141
+ remaining_object_detections_tracks = filter_tracked_objects_by_types(
142
+ metric_cache.future_tracked_objects,
143
+ [t for t in TrackedObjectType if t not in self.get_list_of_simulated_object_types()],
144
+ )
145
+ # the metric cache might contain longer tracks than we simulate, so we truncate the remaining objects' tracks
146
+ remaining_object_detections_tracks = remaining_object_detections_tracks[: len(simulated_detections_tracks)]
147
+ assert (
148
+ len(simulated_detections_tracks) + 1 == simulated_ego_states.shape[0]
149
+ ), f"""
150
+ Traffic agents policy returned trajectories of invalid length:
151
+ {len(simulated_detections_tracks) + 1} != {simulated_ego_states.shape[0]}
152
+ """
153
+
154
+ # merge simulated and log-replay object tracks
155
+ future_detections_tracks = [
156
+ DetectionsTracks(
157
+ TrackedObjects(
158
+ [obj for obj in simulated_detections_tracks[i].tracked_objects]
159
+ + [obj for obj in remaining_object_detections_tracks[i].tracked_objects]
160
+ )
161
+ )
162
+ for i in range(len(simulated_detections_tracks))
163
+ ]
164
+
165
+ return metric_cache.current_tracked_objects + future_detections_tracks
166
+
167
+ @abstractmethod
168
+ def simulate_traffic_agents(
169
+ self, simulated_ego_states: npt.NDArray[np.float64], metric_cache: MetricCache
170
+ ) -> List[DetectionsTracks]:
171
+ """
172
+ Simulates the (reactive) behavior of traffic agents,
173
+ given that the ego agent follows the trajectory provided.
174
+ :param simulated_ego_states: trajectory the ego-vehicle will follow
175
+ :param metric_cache: general metric cache with describing the state of all agents and their environment
176
+ :return: DetectionsTracks object containing the simulated traffic agents
177
+ """
navsim_v2/navsim/traffic_agents_policies/constant_velocity_traffic_agents.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ from nuplan.common.actor_state.agent import Agent
6
+ from nuplan.common.actor_state.oriented_box import OrientedBox
7
+ from nuplan.common.actor_state.state_representation import StateSE2
8
+ from nuplan.common.actor_state.tracked_objects import TrackedObjects
9
+ from nuplan.common.actor_state.tracked_objects_types import TrackedObjectType
10
+ from nuplan.planning.simulation.observation.observation_type import DetectionsTracks
11
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
12
+
13
+ from navsim.planning.metric_caching.metric_cache import MetricCache
14
+ from navsim.traffic_agents_policies.abstract_traffic_agents_policy import (
15
+ AbstractTrafficAgentsPolicy,
16
+ filter_tracked_objects_by_type,
17
+ )
18
+
19
+
20
+ class ConstantVelocityTrafficAgents(AbstractTrafficAgentsPolicy):
21
+ """Naive background traffic agents with constant velocity and constant heading."""
22
+
23
+ def __init__(self, future_trajectory_sampling: TrajectorySampling):
24
+ self.future_trajectory_sampling = future_trajectory_sampling
25
+
26
+ def get_list_of_simulated_object_types(self) -> List[TrackedObjectType]:
27
+ """Inherited, see superclass."""
28
+ return [TrackedObjectType.VEHICLE]
29
+
30
+ def simulate_traffic_agents(
31
+ self, simulated_ego_states: npt.NDArray[np.float64], metric_cache: MetricCache
32
+ ) -> List[DetectionsTracks]:
33
+ """Inherited, see superclass."""
34
+ # extract all vehicle agents in the current frame
35
+ vehicle_current_tracks = filter_tracked_objects_by_type(
36
+ metric_cache.current_tracked_objects, TrackedObjectType.VEHICLE
37
+ )[0]
38
+
39
+ # simulate the future trajectory of the vehicle agents
40
+ future_tracked_objects: List[DetectionsTracks] = []
41
+ for timestep in range(1, self.future_trajectory_sampling.num_poses + 1):
42
+ dt = timestep * self.future_trajectory_sampling.interval_length
43
+ future_tracked_objects_at_dt = []
44
+
45
+ agent: Agent
46
+ for agent in vehicle_current_tracks.tracked_objects:
47
+ future_oriented_box = OrientedBox.from_new_pose(
48
+ box=agent.box,
49
+ pose=StateSE2(
50
+ x=agent.center.x + agent.velocity.x * dt,
51
+ y=agent.center.y + agent.velocity.y * dt,
52
+ heading=agent.center.heading,
53
+ ),
54
+ )
55
+
56
+ future_agent_detection = Agent(
57
+ tracked_object_type=TrackedObjectType.VEHICLE,
58
+ oriented_box=future_oriented_box,
59
+ velocity=agent.velocity,
60
+ metadata=agent.metadata,
61
+ angular_velocity=agent.angular_velocity,
62
+ )
63
+ future_tracked_objects_at_dt.append(future_agent_detection)
64
+
65
+ future_tracked_objects.append(
66
+ DetectionsTracks(tracked_objects=TrackedObjects(tracked_objects=future_tracked_objects_at_dt))
67
+ )
68
+ return future_tracked_objects
navsim_v2/navsim/traffic_agents_policies/log_replay_traffic_agents.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ from nuplan.common.actor_state.tracked_objects import TrackedObjects
6
+ from nuplan.common.actor_state.tracked_objects_types import TrackedObjectType
7
+ from nuplan.planning.simulation.observation.observation_type import DetectionsTracks
8
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
9
+
10
+ from navsim.planning.metric_caching.metric_cache import MetricCache
11
+ from navsim.traffic_agents_policies.abstract_traffic_agents_policy import AbstractTrafficAgentsPolicy
12
+
13
+
14
+ class LogReplayTrafficAgents(AbstractTrafficAgentsPolicy):
15
+ """Replayed (non-reactive) background traffic agents class."""
16
+
17
+ def __init__(self, future_trajectory_sampling: TrajectorySampling):
18
+ self.future_trajectory_sampling = future_trajectory_sampling
19
+
20
+ def get_list_of_simulated_object_types(self) -> List[TrackedObjectType]:
21
+ """Inherited, see superclass."""
22
+ return [t for t in TrackedObjectType]
23
+
24
+ def simulate_traffic_agents(
25
+ self, simulated_ego_states: npt.NDArray[np.float64], metric_cache: MetricCache
26
+ ) -> List[DetectionsTracks]:
27
+ """Inherited, see superclass."""
28
+ raise NotImplementedError("Use simulate_environment instead for this policy")
29
+
30
+ def simulate_environment(
31
+ self, simulated_ego_states: npt.NDArray[np.float64], metric_cache: MetricCache
32
+ ) -> List[DetectionsTracks]:
33
+ """Inherited, see superclass."""
34
+
35
+ ego_box = metric_cache.ego_state.car_footprint.oriented_box.geometry
36
+
37
+ num_poses = self.future_trajectory_sampling.num_poses + 1
38
+ detections_tracks = metric_cache.observation.detections_tracks[:num_poses]
39
+
40
+ first_frame_detections = detections_tracks[0]
41
+
42
+ colliding_agents = {
43
+ agent.metadata.track_token
44
+ for agent in first_frame_detections.tracked_objects.tracked_objects
45
+ if agent.box.geometry.intersects(ego_box)
46
+ }
47
+
48
+ if not colliding_agents:
49
+ return detections_tracks
50
+
51
+ cleaned_detections_tracks = []
52
+ for frame_detections in detections_tracks:
53
+ filtered_agents = [
54
+ agent
55
+ for agent in frame_detections.tracked_objects.tracked_objects
56
+ if agent.metadata.track_token not in colliding_agents
57
+ ]
58
+ cleaned_detections_tracks.append(DetectionsTracks(TrackedObjects(filtered_agents)))
59
+
60
+ return cleaned_detections_tracks
navsim_v2/navsim/traffic_agents_policies/navsim_IDM_traffic_agents.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+ from typing import List, Optional
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ from nuplan.common.actor_state.car_footprint import CarFootprint
7
+ from nuplan.common.actor_state.dynamic_car_state import DynamicCarState
8
+ from nuplan.common.actor_state.ego_state import EgoState
9
+ from nuplan.common.actor_state.state_representation import StateSE2, StateVector2D, TimeDuration, TimePoint
10
+ from nuplan.common.actor_state.tracked_objects_types import TrackedObjectType
11
+ from nuplan.common.maps.nuplan_map.map_factory import get_maps_api
12
+ from nuplan.planning.simulation.observation.observation_type import DetectionsTracks
13
+ from nuplan.planning.simulation.trajectory.trajectory_sampling import TrajectorySampling
14
+
15
+ from navsim.planning.metric_caching.metric_cache import MetricCache
16
+ from navsim.planning.simulation.observation.navsim_idm_agents import NavsimIDMAgents
17
+ from navsim.planning.simulation.planner.pdm_planner.utils.pdm_enums import StateIndex
18
+ from navsim.traffic_agents_policies.abstract_traffic_agents_policy import (
19
+ AbstractTrafficAgentsPolicy,
20
+ filter_tracked_objects_by_type,
21
+ )
22
+
23
+
24
+ def build_ego_state_from_simulated_ego_states(
25
+ ego_state_arr: npt.NDArray[np.float64],
26
+ initial_ego_state: EgoState,
27
+ time_point: TimePoint,
28
+ ) -> EgoState:
29
+ car_footprint = CarFootprint(
30
+ center=StateSE2(
31
+ x=ego_state_arr[StateIndex.X],
32
+ y=ego_state_arr[StateIndex.Y],
33
+ heading=ego_state_arr[StateIndex.HEADING],
34
+ ),
35
+ vehicle_parameters=initial_ego_state.car_footprint.vehicle_parameters,
36
+ )
37
+ rear_axle_acceleration_2d = StateVector2D(
38
+ x=ego_state_arr[StateIndex.ACCELERATION_X],
39
+ y=ego_state_arr[StateIndex.ACCELERATION_Y],
40
+ )
41
+ rear_axle_velocity_2d = StateVector2D(
42
+ x=ego_state_arr[StateIndex.VELOCITY_X],
43
+ y=ego_state_arr[StateIndex.VELOCITY_Y],
44
+ )
45
+ dynamic_car_state = DynamicCarState(
46
+ rear_axle_to_center_dist=initial_ego_state.car_footprint.vehicle_parameters.rear_axle_to_center,
47
+ rear_axle_acceleration_2d=rear_axle_acceleration_2d,
48
+ rear_axle_velocity_2d=rear_axle_velocity_2d,
49
+ angular_acceleration=ego_state_arr[StateIndex.ANGULAR_ACCELERATION],
50
+ tire_steering_rate=ego_state_arr[StateIndex.STEERING_RATE],
51
+ )
52
+ return EgoState(
53
+ car_footprint=car_footprint,
54
+ dynamic_car_state=dynamic_car_state,
55
+ tire_steering_angle=ego_state_arr[StateIndex.STEERING_ANGLE],
56
+ is_in_auto_mode=initial_ego_state.is_in_auto_mode,
57
+ time_point=time_point,
58
+ )
59
+
60
+
61
+ class NavsimIDMTrafficAgents(AbstractTrafficAgentsPolicy):
62
+ def __init__(
63
+ self,
64
+ future_trajectory_sampling: TrajectorySampling,
65
+ idm_agents_observation: NavsimIDMAgents,
66
+ map_root_override: Optional[str] = None,
67
+ ):
68
+ self.future_trajectory_sampling = future_trajectory_sampling
69
+ self._idm_agents_observation: NavsimIDMAgents = idm_agents_observation
70
+ self._map_root_override = map_root_override
71
+
72
+ def get_list_of_simulated_object_types(self) -> List[TrackedObjectType]:
73
+ """Inherited, see superclass."""
74
+ return [TrackedObjectType.VEHICLE]
75
+
76
+ def simulate_traffic_agents(
77
+ self, simulated_ego_states: npt.NDArray[np.float64], metric_cache: MetricCache
78
+ ) -> List[DetectionsTracks]:
79
+ """Inherited, see superclass."""
80
+ # extract all vehicle agents in the current frame
81
+ vehicle_current_tracks = filter_tracked_objects_by_type(
82
+ metric_cache.current_tracked_objects, TrackedObjectType.VEHICLE
83
+ )[0]
84
+
85
+ # egostate
86
+ initial_ego_state = metric_cache.ego_state
87
+ # map api
88
+ map_root = self._map_root_override or metric_cache.map_parameters.map_root
89
+ map_api = get_maps_api(
90
+ map_root,
91
+ metric_cache.map_parameters.map_version,
92
+ metric_cache.map_parameters.map_name,
93
+ )
94
+ # extract future tracked objects
95
+ objects_future_tracks = metric_cache.future_tracked_objects
96
+ # traffic light status
97
+ traffic_light_status = getattr(metric_cache, "traffic_light_status", None)
98
+
99
+ # we need to make a fresh copy of the idm_agents_observation
100
+ # otherwise its state will leak into other simulations
101
+ idm_agents_observation = deepcopy(self._idm_agents_observation)
102
+
103
+ # current observation
104
+ idm_agents_observation.get_observation(
105
+ initial_ego_state, vehicle_current_tracks, map_api, objects_future_tracks[0].tracked_objects
106
+ )
107
+
108
+ # build EgoStates from the simulated_ego_states array
109
+ ego_future_trajectory: List[EgoState] = []
110
+ for t, ego_state_arr in enumerate(simulated_ego_states):
111
+ ego_future_trajectory.append(
112
+ build_ego_state_from_simulated_ego_states(
113
+ ego_state_arr=ego_state_arr,
114
+ initial_ego_state=initial_ego_state,
115
+ time_point=initial_ego_state.time_point + TimeDuration.from_s((t + 1) * 0.1),
116
+ )
117
+ )
118
+
119
+ # simulate the future trajectory of the vehicle agents
120
+ future_tracked_objects: List[DetectionsTracks] = []
121
+ for timestep in range(1, self.future_trajectory_sampling.num_poses + 1):
122
+
123
+ ego_state = ego_future_trajectory[timestep - 1]
124
+
125
+ idm_agents_observation.update_observation(
126
+ timestep,
127
+ ego_state,
128
+ vehicle_current_tracks,
129
+ map_api,
130
+ objects_future_tracks[timestep - 1].tracked_objects,
131
+ traffic_light_status[timestep] if traffic_light_status is not None else None,
132
+ )
133
+ future_tracked_objects.append(
134
+ idm_agents_observation.get_observation(
135
+ ego_state, vehicle_current_tracks, map_api, objects_future_tracks[timestep - 1].tracked_objects
136
+ )
137
+ )
138
+
139
+ return future_tracked_objects
navsim_v2/requirements.txt ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ nuplan-devkit @ git+https://github.com/motional/nuplan-devkit/@nuplan-devkit-v1.2
2
+ scikit-learn==1.2.2
3
+ positional-encodings==6.0.1
4
+
5
+ # nuplan requirements
6
+ aioboto3
7
+ aiofiles
8
+ bokeh==2.4.3 # Used in the nuBoard dashboard
9
+ casadi # Used for optimization solving
10
+ control==0.9.1 # Used for LQR controller synthesis
11
+ Fiona # Used in GpkgMapsDB.py
12
+ geopandas>=0.12.1 # Used to read maps
13
+ guppy3==3.1.2
14
+ hydra-core==1.2.0 # Used for configs
15
+ joblib
16
+ matplotlib # Used for rendering
17
+ nest_asyncio # Used to nest event loops when running nuBoard/jupyter
18
+ numpy==1.23.4
19
+ opencv-python==4.9.0.80 # Used widely
20
+ pandas # Used widely
21
+ Pillow # Used widely to render images
22
+ psutil # Used widely to get the number of CPUs
23
+ pyarrow # For parquet
24
+ pyinstrument # Used widely as profiler
25
+ pyogrio # A performant backend for geopandas
26
+ pyquaternion>=0.9.5 # Used widely, avoid known bug with quaternion slerp
27
+ pytest # Used widely
28
+ rasterio # Used in GpkgMapsDB.py
29
+ ray # Used widely
30
+ retry
31
+ rtree # Used in occupancy maps
32
+ scipy # Used widely
33
+ selenium # Used in bokeh export png
34
+ setuptools==65.5.1 # Used in setup.py, pinned to not break pytorch
35
+ Shapely>=2.0.0 # Used widely
36
+ SQLAlchemy==1.4.27 # older versions don't work with some table definitions
37
+ sympy # Use for symbolic algebra
38
+ tornado # Used in nuboard.py
39
+ tqdm # Used widely
40
+ ujson # Used in serialiation_callback.py
41
+
navsim_v2/scripts/evaluation/eval_posttraj_temporal_v22.sh ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Independent temporal reranking experiment; existing evaluation scripts unchanged.
3
+ set -euo pipefail
4
+ NAVSIM_V2_ROOT="${NAVSIM_V2_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
5
+ CONDA_ROOT="${CONDA_ROOT:-/home/myuser/miniconda3/envs/drive-jepa}"
6
+ export OPENSCENE_DATA_ROOT="${OPENSCENE_DATA_ROOT:-/mnt/c2-worldmodel/training_data/OpenScene/dataset}"
7
+ export NAVSIM_EXP_ROOT="${NAVSIM_EXP_ROOT:-/mnt/c2-worldmodel/2639639/navsim_exp}"
8
+ export NUPLAN_MAPS_ROOT="${OPENSCENE_DATA_ROOT}/maps"
9
+ export NUPLAN_MAP_VERSION=nuplan-maps-v1.0
10
+ export NAVSIM_DEVKIT_ROOT="${NAVSIM_V2_ROOT}"
11
+ export PYTHONNOUSERSITE=1
12
+ export PYTHONPATH="${NAVSIM_V2_ROOT}:${NAVSIM_V2_ROOT}/vjepa2:${PYTHONPATH:-}"
13
+ export LD_LIBRARY_PATH="${CONDA_ROOT}/lib/python3.9/site-packages/torch/lib:${CONDA_ROOT}/lib:${LD_LIBRARY_PATH:-}"
14
+ export NAVSIM_AGENT_CUDA=1 NAVSIM_GPU_WORKER_AFFINITY=1
15
+ SPLIT="${SPLIT:-navtest}"
16
+ case "${SPLIT}" in
17
+ navtest)
18
+ ENTRY=run_pdm_score_one_stage.py
19
+ CACHE_DEFAULT="${NAVSIM_EXP_ROOT}/Drive-JEPA-cache/metric_cache_v2"
20
+ ;;
21
+ navhard_two_stage)
22
+ ENTRY=run_pdm_score.py
23
+ CACHE_DEFAULT="${NAVSIM_EXP_ROOT}/navhard_two_stage_metric_cache_v22"
24
+ ;;
25
+ *) echo "Unsupported split: ${SPLIT}" >&2; exit 1 ;;
26
+ esac
27
+ MODE="${MODE:-bounded}"
28
+ CHECKPOINT_PATH="${CHECKPOINT_PATH:-/tmp/posttraj_v1_e1_zeroshot_v2.ckpt}"
29
+ METRIC_CACHE_PATH="${METRIC_CACHE_PATH:-${CACHE_DEFAULT}}"
30
+ [[ -s "${CHECKPOINT_PATH}" && -d "${METRIC_CACHE_PATH}" ]]
31
+ cd "${NAVSIM_V2_ROOT}"
32
+ CUDA_VISIBLE_DEVICES="${GPU:-0,1}" "${CONDA_ROOT}/bin/python" -u \
33
+ "navsim/planning/script/${ENTRY}" --config-name=posttraj_temporal_v22 \
34
+ "train_test_split=${SPLIT}" agent=drive_jepa_posttraj_v2_agent \
35
+ "agent.checkpoint_path=${CHECKPOINT_PATH}" "metric_cache_path=${METRIC_CACHE_PATH}" \
36
+ worker=single_machine_thread_pool worker.use_process_pool=true \
37
+ "worker.max_workers=${WORKERS:-16}" \
38
+ "experiment_name=${EXPERIMENT_NAME:-posttraj_v1_${SPLIT}_temporal_${MODE}_v22}" \
39
+ "temporal_rerank.mode=${MODE}" "$@"
navsim_v2/setup.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import setuptools
4
+
5
+ # Change directory to allow installation from anywhere
6
+ script_folder = os.path.dirname(os.path.realpath(__file__))
7
+ os.chdir(script_folder)
8
+
9
+ with open("requirements.txt") as f:
10
+ requirements = f.read().splitlines()
11
+
12
+ # Installs
13
+ setuptools.setup(
14
+ name="navsim",
15
+ version="2.0.0",
16
+ author="University of Tuebingen",
17
+ author_email="kashyap.chitta@uni-tuebingen.de",
18
+ description="NAVSIM: Data-Driven Non-Reactive Autonomous Vehicle Simulation and Benchmarking",
19
+ url="https://github.com/autonomousvision/navsim",
20
+ python_requires=">=3.9",
21
+ packages=setuptools.find_packages(script_folder),
22
+ package_dir={"": "."},
23
+ classifiers=[
24
+ "Programming Language :: Python :: 3.9",
25
+ "Operating System :: OS Independent",
26
+ "License :: Free for non-commercial use",
27
+ ],
28
+ license="apache-2.0",
29
+ install_requires=requirements,
30
+ )
scripts/eval_navsim_v1.sh ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ source "$ROOT/environment/activate_model.sh"
5
+ export OPENSCENE_DATA_ROOT="${OPENSCENE_DATA_ROOT:-/mnt/c2-worldmodel/training_data/OpenScene/dataset}"
6
+ export NAVSIM_EXP_ROOT="${NAVSIM_EXP_ROOT:-/mnt/c2-worldmodel/2639639/navsim_exp}"
7
+ export NUPLAN_MAPS_ROOT="${NUPLAN_MAPS_ROOT:-$OPENSCENE_DATA_ROOT/maps}" NUPLAN_MAP_VERSION=nuplan-maps-v1.0
8
+ config_only=false
9
+ for arg in "$@"; do [[ "$arg" == --cfg* ]] && config_only=true; done
10
+ if [[ "$config_only" == false ]]; then
11
+ : "${OUTPUT_DIR:?Set OUTPUT_DIR to a new results directory}"
12
+ [[ ! -e "$OUTPUT_DIR" ]] || { echo 'OUTPUT_DIR already exists' >&2; exit 2; }
13
+ fi
14
+ cd "$ROOT/navsim_v1"
15
+ extra=()
16
+ [[ -z "${METRIC_CACHE_PATH:-}" ]] || extra+=("metric_cache_path=$METRIC_CACHE_PATH")
17
+ exec "$MODEL_ENV/bin/python" navsim/planning/script/run_pdm_score.py \
18
+ train_test_split=navtest agent=drive_jepa_perception_based_agent \
19
+ +agent.config.vjepa_version=2.1 "+agent.config.pretrain_pt_path=$ROOT/checkpoints/vjepa2_1_vitl.pt" \
20
+ +agent.config.image_architecture=vjepa2_1_vit_large_384 +agent.config.freeze_encoder=True \
21
+ +agent.config.use_lora=True +agent.config.lora_rank=32 +agent.config.proposal_num=32 \
22
+ agent.config.posttraj_future_enabled=True agent.config.posttraj_future_loss_weight=0.1 \
23
+ agent.config.posttraj_future_ema_decay=0.99925 'agent.config.posttraj_future_frame_offsets=[1]' \
24
+ agent.config.posttraj_future_candidate_layers=2 agent.config.posttraj_future_full_decoder_layers=2 \
25
+ agent.config.posttraj_scorer_temporal_layers=1 agent.config.posttraj_future_layernorm_target=True \
26
+ agent.config.posttraj_detach_trajectory_inputs=True agent.config.posttraj_stage2_enabled=False \
27
+ agent.config.posttraj_joint_enabled=False "agent.config.anchor_trajectory_path=$ROOT/navsim_v1/data/8192.npy" \
28
+ +agent.config.sub_score_weight=0 +agent.config.final_score_weight=1 \
29
+ "agent.checkpoint_path=${CHECKPOINT_PATH:-$ROOT/checkpoints/navsim_final.ckpt}" \
30
+ experiment_name=da_wam_final_v1 "output_dir=${OUTPUT_DIR:-/tmp/da_wam_v1_config_only}" \
31
+ worker=single_machine_thread_pool "worker.max_workers=${WORKERS:-4}" worker.use_process_pool=true \
32
+ "${extra[@]}" "$@"
scripts/eval_navsim_v2.sh ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ source "$ROOT/environment/activate_model.sh"
5
+ export NAVSIM_V2_ROOT="$ROOT/navsim_v2" CONDA_ROOT="$MODEL_ENV"
6
+ export CHECKPOINT_PATH="${CHECKPOINT_PATH:-$ROOT/checkpoints/navsim_final.ckpt}" MODE=momentum
7
+ config_only=false
8
+ for arg in "$@"; do [[ "$arg" == --cfg* ]] && config_only=true; done
9
+ if [[ "$config_only" == false ]]; then
10
+ : "${OUTPUT_DIR:?Set OUTPUT_DIR to a new results directory}"
11
+ [[ ! -e "$OUTPUT_DIR" ]] || { echo 'OUTPUT_DIR already exists' >&2; exit 2; }
12
+ fi
13
+ exec bash "$ROOT/navsim_v2/scripts/evaluation/eval_posttraj_temporal_v22.sh" \
14
+ "agent.config.pretrain_pt_path=$ROOT/checkpoints/vjepa2_1_vitl.pt" \
15
+ temporal_rerank.weight=0.125 "output_dir=${OUTPUT_DIR:-/tmp/da_wam_v2_config_only}" "$@"
scripts/prepare_bench2drive_eval.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Validate or prepare a fresh single-worker final-model Bench2Drive evaluation."""
2
+ import argparse,json,os,shlex
3
+ from pathlib import Path
4
+ p=argparse.ArgumentParser();p.add_argument('--check',action='store_true');p.add_argument('--output',type=Path);a=p.parse_args()
5
+ root=Path(__file__).resolve().parents[1]; frozen=root/'bench2drive/evaluation/final_4cam'
6
+ config=json.loads((frozen/'4cam_score11/config.json').read_text())
7
+ for name in ['checkpoint','hydra_config','model_root']:
8
+ path=Path(config['model'][name]);assert path.exists(),(name,path)
9
+ assert Path(config['model_launcher']).is_file()
10
+ if a.check:print(json.dumps({'status':'paths_ok','checkpoint':config['model']['checkpoint'],'evaluation_started':False},indent=2))
11
+ if a.output:
12
+ out=a.output.resolve();out.mkdir(parents=True,exist_ok=False)
13
+ config['model']['checkpoint']=str(root/'checkpoints/bench2drive_4cam_score11.ckpt')
14
+ (out/'config.json').write_text(json.dumps(config,indent=2))
15
+ def q(x):return shlex.quote(str(x))
16
+ script='#!/usr/bin/env bash\nset -euo pipefail\n'
17
+ script+='export DRIVE_JEPA_B2D_CONFIG='+q(out/'config.json')+'\nexport DRIVE_JEPA_B2D_RESULT_DIR='+q(out/'results')+'\n'
18
+ script+='export CARLA_GPU="${CARLA_GPU:-0}" DRIVE_JEPA_GPU="${DRIVE_JEPA_GPU:-1}"\n'
19
+ script+='export CARLA_PORT="${CARLA_PORT:-2600}" TM_PORT="${TM_PORT:-8600}" DRIVE_JEPA_BRIDGE_PORT="${DRIVE_JEPA_BRIDGE_PORT:-50800}"\n'
20
+ script+='export OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 PYTHONDONTWRITEBYTECODE=1\n'
21
+ script+='exec bash '+q(frozen/'resume.sh')+'\n'
22
+ (out/'run.sh').write_text(script);(out/'run.sh').chmod(0o755)
23
+ print('Prepared '+str(out/'run.sh')+'; no evaluation started.')
24
+ if not a.check and not a.output:p.error('provide --check or --output')
scripts/train_bench2drive.sh ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ exec bash "$ROOT/bench2drive/evaluation/final_4cam/sources/4cam/train_bench2drive.sh" "$@"
scripts/train_navsim.sh ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
4
+ source "$ROOT/environment/activate_model.sh"
5
+ cd "$ROOT/navsim_v1"
6
+ case "${1:-}" in
7
+ stage1) shift; exec bash scripts/training/run_posttraj_future_joint30.sh "$@" ;;
8
+ stage2) shift; : "${CHECKPOINT_PATH:?Set CHECKPOINT_PATH to your Stage 1 epoch 15 checkpoint}"; export CHECKPOINT_PATH; exec bash scripts/training/run_posttraj_joint_e15_safety_final.sh "$@" ;;
9
+ *) echo 'Usage: train_navsim.sh stage1|stage2' >&2;exit 2 ;;
10
+ esac
third_party/vjepa2/APACHE-LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2018-2021 William Falcon
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
third_party/vjepa2/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) Meta Platforms, Inc. and affiliates.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
third_party/vjepa2/evals/hub/__init__.py ADDED
File without changes
third_party/vjepa2/evals/hub/preprocessor.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the MIT license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+
7
+ def _make_transforms(crop_size=256):
8
+ from ..video_classification_frozen.utils import make_transforms
9
+
10
+ return make_transforms(crop_size=crop_size, training=False)
11
+
12
+
13
+ def vjepa2_preprocessor(*, pretrained: bool = True, **kwargs):
14
+ crop_size = kwargs.get("crop_size", 256)
15
+ return _make_transforms(crop_size=crop_size)
third_party/vjepa2/hubconf.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the MIT license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ from evals.hub.preprocessor import vjepa2_preprocessor
7
+ from src.hub.backbones import (
8
+ vjepa2_ac_vit_giant,
9
+ vjepa2_vit_giant,
10
+ vjepa2_vit_giant_384,
11
+ vjepa2_vit_huge,
12
+ vjepa2_vit_large,
13
+ vjepa2_1_vit_base_384,
14
+ vjepa2_1_vit_large_384,
15
+ vjepa2_1_vit_giant_384,
16
+ vjepa2_1_vit_gigantic_384,
17
+ )
18
+
19
+ dependencies = ["torch", "timm", "einops"]
third_party/vjepa2/pyproject.toml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [tool.isort]
2
+ profile="black"
3
+ line_length=119
4
+
5
+ [tool.black]
6
+ line-length = 119
third_party/vjepa2/requirements.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=2
2
+ torchvision
3
+ tensorboard
4
+ wandb
5
+ iopath
6
+ pyyaml
7
+ numpy
8
+ opencv-python
9
+ submitit
10
+ braceexpand
11
+ webdataset
12
+ timm
13
+ transformers
14
+ peft
15
+ decord
16
+ pandas
17
+ einops
18
+ beartype
19
+ psutil
20
+ h5py
21
+ fire
22
+ python-box
23
+ scikit-image
24
+ ftfy
25
+ jupyter