Upload latest curated NAVSIM and Bench2Drive release with final checkpoints (part 4)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/proposal/pdm_generator.py +382 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/proposal/pdm_proposal.py +96 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/__init__.py +0 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_and_traffic_scorer.py +277 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_comfort_metrics.py +467 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_scorer.py +700 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/pdm_scorer_utils.py +64 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/scoring/scene_aggregator.py +124 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/__init__.py +0 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/batch_kinematic_bicycle.py +184 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/batch_lqr.py +457 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/batch_lqr_utils.py +238 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/simulation/pdm_simulator.py +78 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/__init__.py +0 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/graph_search/__init__.py +0 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/graph_search/bfs_roadblock.py +146 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/graph_search/dijkstra.py +145 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_array_representation.py +282 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_emergency_brake.py +135 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_enums.py +181 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_geometry_utils.py +178 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/pdm_path.py +106 -0
- navsim_v2/navsim/planning/simulation/planner/pdm_planner/utils/route_utils.py +215 -0
- navsim_v2/navsim/planning/training/__init__.py +0 -0
- navsim_v2/navsim/planning/training/abstract_feature_target_builder.py +46 -0
- navsim_v2/navsim/planning/training/agent_lightning_module.py +126 -0
- navsim_v2/navsim/planning/training/callbacks/time_logging_callback.py +52 -0
- navsim_v2/navsim/planning/training/dataset.py +288 -0
- navsim_v2/navsim/planning/utils/multithreading/__init__.py +0 -0
- navsim_v2/navsim/planning/utils/multithreading/worker_ray_no_torch.py +159 -0
- navsim_v2/navsim/traffic_agents_policies/__init__.py +0 -0
- navsim_v2/navsim/traffic_agents_policies/abstract_traffic_agents_policy.py +177 -0
- navsim_v2/navsim/traffic_agents_policies/constant_velocity_traffic_agents.py +68 -0
- navsim_v2/navsim/traffic_agents_policies/log_replay_traffic_agents.py +60 -0
- navsim_v2/navsim/traffic_agents_policies/navsim_IDM_traffic_agents.py +139 -0
- navsim_v2/requirements.txt +41 -0
- navsim_v2/scripts/evaluation/eval_posttraj_temporal_v22.sh +39 -0
- navsim_v2/setup.py +30 -0
- scripts/eval_navsim_v1.sh +32 -0
- scripts/eval_navsim_v2.sh +15 -0
- scripts/prepare_bench2drive_eval.py +24 -0
- scripts/train_bench2drive.sh +4 -0
- scripts/train_navsim.sh +10 -0
- third_party/vjepa2/APACHE-LICENSE +201 -0
- third_party/vjepa2/LICENSE +21 -0
- third_party/vjepa2/evals/hub/__init__.py +0 -0
- third_party/vjepa2/evals/hub/preprocessor.py +15 -0
- third_party/vjepa2/hubconf.py +19 -0
- third_party/vjepa2/pyproject.toml +6 -0
- 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
|