scenario-characterization 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- characterization/__init__.py +0 -0
- characterization/features/__init__.py +14 -0
- characterization/features/base_feature.py +45 -0
- characterization/features/individual_features.py +158 -0
- characterization/features/individual_utils.py +196 -0
- characterization/features/interaction_features.py +153 -0
- characterization/features/interaction_utils.py +252 -0
- characterization/probing/__init__.py +0 -0
- characterization/processors/__init__.py +0 -0
- characterization/processors/base_processor.py +78 -0
- characterization/processors/feature_processor.py +61 -0
- characterization/processors/scores_processor.py +92 -0
- characterization/run_processor.py +48 -0
- characterization/scorer/__init__.py +1 -0
- characterization/scorer/base_scorer.py +82 -0
- characterization/scorer/individual_scorer.py +97 -0
- characterization/scorer/interaction_scorer.py +88 -0
- characterization/utils/__init__.py +0 -0
- characterization/utils/common.py +130 -0
- characterization/utils/datasets/__init__.py +0 -0
- characterization/utils/datasets/dataset.py +155 -0
- characterization/utils/datasets/waymo.py +380 -0
- characterization/utils/datasets/waymo_preprocess.py +412 -0
- characterization/utils/schemas.py +140 -0
- characterization/utils/viz/__init__.py +0 -0
- characterization/utils/viz/visualizer.py +73 -0
- characterization/utils/viz/waymo.py +768 -0
- characterization/viz_scores_pdf.py +201 -0
- scenario_characterization-0.1.0.dist-info/LICENSE +201 -0
- scenario_characterization-0.1.0.dist-info/METADATA +288 -0
- scenario_characterization-0.1.0.dist-info/RECORD +33 -0
- scenario_characterization-0.1.0.dist-info/WHEEL +5 -0
- scenario_characterization-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
from omegaconf import DictConfig
|
|
5
|
+
|
|
6
|
+
from characterization.utils.schemas import Scenario, ScenarioFeatures
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseFeature(ABC):
|
|
10
|
+
def __init__(self, config: DictConfig) -> None:
|
|
11
|
+
"""Initializes the BaseFeature with a configuration.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
config (DictConfig): Configuration for the feature.
|
|
15
|
+
"""
|
|
16
|
+
self.config = config
|
|
17
|
+
self.features = config.features
|
|
18
|
+
self.characterizer_type = "feature"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
"""Gets the class name formatted as a lowercase string with spaces.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
str: The formatted class name (e.g., 'base feature').
|
|
26
|
+
"""
|
|
27
|
+
# Get the class name and add a space before each capital letter (except the first)
|
|
28
|
+
return re.sub(r"(?<!^)([A-Z])", r" \1", self.__class__.__name__).lower()
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def compute(self, scenario: Scenario) -> ScenarioFeatures:
|
|
32
|
+
"""Computes features for a given scenario.
|
|
33
|
+
|
|
34
|
+
This method should be overridden by subclasses to compute actual features.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
scenario (Scenario): Scenario data to compute features for.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
ScenarioFeatures: Computed features for the scenario.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If the scenario does not contain the required information.
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from omegaconf import DictConfig
|
|
3
|
+
|
|
4
|
+
import characterization.features.individual_utils as individual
|
|
5
|
+
from characterization.features.base_feature import BaseFeature
|
|
6
|
+
from characterization.utils.common import get_logger
|
|
7
|
+
from characterization.utils.schemas import Scenario, ScenarioFeatures
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IndividualFeatures(BaseFeature):
|
|
13
|
+
def __init__(self, config: DictConfig) -> None:
|
|
14
|
+
"""Initializes the IndividualFeatures class.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
config (DictConfig): Configuration for the feature. Expected to contain key-value pairs
|
|
18
|
+
relevant to feature computation, such as thresholds or parameters. Must include
|
|
19
|
+
'return_criteria' (str), which determines whether to return 'critical' or 'average'
|
|
20
|
+
statistics for each feature.
|
|
21
|
+
"""
|
|
22
|
+
super(IndividualFeatures, self).__init__(config)
|
|
23
|
+
|
|
24
|
+
self.return_criteria = config.get("return_criteria", "critical")
|
|
25
|
+
|
|
26
|
+
def compute(self, scenario: Scenario) -> ScenarioFeatures:
|
|
27
|
+
"""Computes features for each agent in the scenario.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
scenario (Scenario): Scenario object containing agent positions, velocities, validity masks,
|
|
31
|
+
timestamps, map conflict points, stationary speed, and other scenario-level information.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
ScenarioFeatures: An object containing computed features for each valid agent, including
|
|
35
|
+
speed, speed limit difference, acceleration, deceleration, jerk, waiting period,
|
|
36
|
+
waiting interval, waiting distance, and agent-to-agent closest distances.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If an unknown return criteria is provided in the configuration.
|
|
40
|
+
"""
|
|
41
|
+
agent_positions = scenario.agent_positions
|
|
42
|
+
agent_velocities = scenario.agent_velocities
|
|
43
|
+
agent_valid = scenario.agent_valid
|
|
44
|
+
scenario_timestamps = scenario.timestamps
|
|
45
|
+
conflict_points = scenario.map_conflict_points
|
|
46
|
+
stationary_speed = scenario.stationary_speed
|
|
47
|
+
|
|
48
|
+
# Meta information to be included within ScenarioFeatures. For an agent to be valid it needs to have at least
|
|
49
|
+
# two valid timestamps. The indeces of such agents will be added to `valid_idxs` list.
|
|
50
|
+
scenario_valid_idxs = []
|
|
51
|
+
|
|
52
|
+
# Features to be included in ScenarioFeatures
|
|
53
|
+
scenario_speeds = []
|
|
54
|
+
scenario_speed_limit_diffs = []
|
|
55
|
+
scenario_accelerations = []
|
|
56
|
+
scenario_decelerations = []
|
|
57
|
+
scenario_jerks = []
|
|
58
|
+
scenario_waiting_periods = []
|
|
59
|
+
scenario_waiting_intervals = []
|
|
60
|
+
scenario_waiting_distances = []
|
|
61
|
+
|
|
62
|
+
# NOTE: Handling sequentially since each agent may have different valid masks which will
|
|
63
|
+
# result in trajectories of different lengths.
|
|
64
|
+
for n in range(scenario.num_agents):
|
|
65
|
+
mask = agent_valid[n].squeeze(-1)
|
|
66
|
+
if not mask.any() or mask.sum() < 2:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
velocities = agent_velocities[n][mask, :]
|
|
70
|
+
positions = agent_positions[n][mask, :]
|
|
71
|
+
timestamps = scenario_timestamps[mask]
|
|
72
|
+
|
|
73
|
+
# Compute agent features
|
|
74
|
+
|
|
75
|
+
# Speed Profile
|
|
76
|
+
speeds, speed_limit_diffs = individual.compute_speed(velocities)
|
|
77
|
+
if speeds is None or speed_limit_diffs is None:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# Acceleration/Deceleration Profile
|
|
81
|
+
# NOTE: acc and dec are accumulated abs acceleration and deceleration profiles.
|
|
82
|
+
_, accelerations, decelerations = individual.compute_acceleration_profile(speeds, timestamps)
|
|
83
|
+
if accelerations is None or decelerations is None:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Jerk Profile
|
|
87
|
+
jerks = individual.compute_jerk(speeds, timestamps)
|
|
88
|
+
|
|
89
|
+
# Waiting period
|
|
90
|
+
waiting_periods, waiting_intervals, waiting_distances = individual.compute_waiting_period(
|
|
91
|
+
positions,
|
|
92
|
+
speeds,
|
|
93
|
+
timestamps,
|
|
94
|
+
conflict_points,
|
|
95
|
+
stationary_speed,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if self.return_criteria == "critical":
|
|
99
|
+
speed = speeds.max()
|
|
100
|
+
speed_limit_diff = speed_limit_diffs.max()
|
|
101
|
+
acceleration = accelerations.max()
|
|
102
|
+
deceleration = decelerations.max()
|
|
103
|
+
jerk = jerks.max()
|
|
104
|
+
waiting_period = waiting_periods.max()
|
|
105
|
+
waiting_interval = waiting_intervals.max()
|
|
106
|
+
waiting_distance = waiting_distances.min()
|
|
107
|
+
|
|
108
|
+
elif self.return_criteria == "average":
|
|
109
|
+
speed = speeds.mean()
|
|
110
|
+
speed_limit_diff = speed_limit_diffs.mean()
|
|
111
|
+
acceleration = accelerations.mean()
|
|
112
|
+
deceleration = decelerations.mean()
|
|
113
|
+
jerk = jerks.mean()
|
|
114
|
+
waiting_period = waiting_periods.mean()
|
|
115
|
+
waiting_interval = waiting_intervals.mean()
|
|
116
|
+
waiting_distance = waiting_distances.mean()
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"Unknown return criteria: {self.return_criteria}")
|
|
120
|
+
|
|
121
|
+
scenario_valid_idxs.append(n)
|
|
122
|
+
scenario_speeds.append(speed)
|
|
123
|
+
scenario_speed_limit_diffs.append(speed_limit_diff)
|
|
124
|
+
scenario_accelerations.append(acceleration)
|
|
125
|
+
scenario_decelerations.append(deceleration)
|
|
126
|
+
scenario_jerks.append(jerk)
|
|
127
|
+
scenario_waiting_periods.append(waiting_period)
|
|
128
|
+
scenario_waiting_intervals.append(waiting_interval)
|
|
129
|
+
scenario_waiting_distances.append(waiting_distance)
|
|
130
|
+
|
|
131
|
+
# NOTE: this is not really an individual feature and would be useful for interactive features.
|
|
132
|
+
agent_to_agent_closest_dists = (
|
|
133
|
+
np.linalg.norm(agent_positions[:, np.newaxis, :] - agent_positions[np.newaxis, :, :], axis=-1)
|
|
134
|
+
.min(axis=-1)
|
|
135
|
+
.astype(np.float32)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return ScenarioFeatures(
|
|
139
|
+
num_agents=scenario.num_agents,
|
|
140
|
+
scenario_id=scenario.scenario_id,
|
|
141
|
+
agent_types=scenario.agent_types,
|
|
142
|
+
valid_idxs=np.array(scenario_valid_idxs, dtype=np.int32) if scenario_valid_idxs else None,
|
|
143
|
+
speed=np.array(scenario_speeds, dtype=np.float32) if scenario_speeds else None,
|
|
144
|
+
speed_limit_diff=(
|
|
145
|
+
np.array(scenario_speed_limit_diffs, dtype=np.float32) if scenario_speed_limit_diffs else None
|
|
146
|
+
),
|
|
147
|
+
acceleration=np.array(scenario_accelerations, dtype=np.float32) if scenario_accelerations else None,
|
|
148
|
+
deceleration=np.array(scenario_decelerations, dtype=np.float32) if scenario_decelerations else None,
|
|
149
|
+
jerk=np.array(scenario_jerks, dtype=np.float32) if scenario_jerks else None,
|
|
150
|
+
waiting_period=np.array(scenario_waiting_periods, dtype=np.float32) if scenario_waiting_periods else None,
|
|
151
|
+
waiting_interval=(
|
|
152
|
+
np.array(scenario_waiting_intervals, dtype=np.float32) if scenario_waiting_intervals else None
|
|
153
|
+
),
|
|
154
|
+
waiting_distance=(
|
|
155
|
+
np.array(scenario_waiting_distances, dtype=np.float32) if scenario_waiting_distances else None
|
|
156
|
+
),
|
|
157
|
+
agent_to_agent_closest_dists=agent_to_agent_closest_dists,
|
|
158
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from characterization.utils.common import EPS, get_logger
|
|
4
|
+
|
|
5
|
+
logger = get_logger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_speed(velocities: np.ndarray) -> tuple[np.ndarray | None, ...]:
|
|
9
|
+
"""Computes the speed profile of an agent.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
velocities (np.ndarray): The velocity vectors of the agent over time (shape: [T, D]).
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
tuple:
|
|
16
|
+
speeds (np.ndarray or None): The speed time series (shape: [T,]), or None if NaN values are present.
|
|
17
|
+
speeds_limit_diff (np.ndarray or None): The difference between speed and speed limit (currently zeros),
|
|
18
|
+
or None if NaN values are present.
|
|
19
|
+
"""
|
|
20
|
+
speeds = np.linalg.norm(velocities, axis=-1)
|
|
21
|
+
if np.isnan(speeds).any():
|
|
22
|
+
logger.warning(f"Nan value in agent speed: {speeds}")
|
|
23
|
+
return None, None
|
|
24
|
+
|
|
25
|
+
# -----------------------------------------------------------------------------------------
|
|
26
|
+
# TODO: Add speed limit difference feature. Depends on the context and lane information.
|
|
27
|
+
speeds_limit_diff = np.zeros_like(speeds, dtype=np.float32)
|
|
28
|
+
# speed_limits = np.zeros(velocities.shape[0])
|
|
29
|
+
# in_lane = np.zeros(velocities.shape[0]).astype(bool)
|
|
30
|
+
|
|
31
|
+
# for i in range(lane_idx.shape[0]):
|
|
32
|
+
# speed_limits[i] = speed_limits[-1]
|
|
33
|
+
# if lane_idx[i][0] > 0 and lane_idx[i][0] < len(lane_info):
|
|
34
|
+
# speed_limits[i] = mph_to_ms(lane_info[int(lane_idx[i][0])]['speed_limit_mph'])
|
|
35
|
+
# in_lane[i] = True
|
|
36
|
+
# else:
|
|
37
|
+
# in_lane[i] = False
|
|
38
|
+
# speed_limit_diff = speed - speed_limits
|
|
39
|
+
# -----------------------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
return speeds, speeds_limit_diff
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def compute_acceleration_profile(speed: np.ndarray, timestamps: np.ndarray) -> tuple[np.ndarray | None, ...]:
|
|
45
|
+
"""Computes the acceleration profile from the speed (m/s) and time delta.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
speed (np.ndarray): The speed time series (m/s) (shape: [T,]).
|
|
49
|
+
timestamps (np.ndarray): The timestamps corresponding to each speed measurement (shape: [T,]).
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
tuple:
|
|
53
|
+
acceleration_raw (np.ndarray or None): The raw acceleration time series (shape: [T,]), or None if NaN values
|
|
54
|
+
are present.
|
|
55
|
+
acceleration (np.ndarray or None): The sum of positive acceleration intervals, or None if NaN values are
|
|
56
|
+
present.
|
|
57
|
+
deceleration (np.ndarray or None): The sum of negative acceleration intervals (absolute value), or None if
|
|
58
|
+
NaN values are present.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If speed and timestamps do not have the same shape.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def get_acc_sums(acc: np.array, idx: np.array) -> tuple[np.array, np.array]:
|
|
65
|
+
diff = idx[1:] - idx[:-1]
|
|
66
|
+
diff = np.array([-1] + np.where(diff > 1)[0].tolist() + [diff.shape[0]])
|
|
67
|
+
se_idxs = [(idx[s + 1], idx[e] + 1) for s, e in zip(diff[:-1], diff[1:])]
|
|
68
|
+
sums = np.array([acc[s:e].sum() for s, e in se_idxs])
|
|
69
|
+
return sums, se_idxs
|
|
70
|
+
|
|
71
|
+
if not speed.shape == timestamps.shape:
|
|
72
|
+
raise ValueError("Speed and timestamps must have the same shape.")
|
|
73
|
+
|
|
74
|
+
acceleration_raw = np.gradient(speed, timestamps) # m/s^2
|
|
75
|
+
|
|
76
|
+
if np.isnan(acceleration_raw).any():
|
|
77
|
+
logger.warning(f"Nan value in agent acceleration: {acceleration_raw}")
|
|
78
|
+
return None, None, None
|
|
79
|
+
|
|
80
|
+
dr_idx = np.where(acceleration_raw < 0.0)[0]
|
|
81
|
+
|
|
82
|
+
# If the agent is accelerating or maintaining acceleration
|
|
83
|
+
if dr_idx.shape[0] == 0:
|
|
84
|
+
deceleration = np.zeros(shape=(1,))
|
|
85
|
+
acceleration = acceleration_raw.copy()
|
|
86
|
+
# If the agent is decelerating
|
|
87
|
+
elif dr_idx.shape[0] == acceleration_raw.shape[0]:
|
|
88
|
+
deceleration = acceleration_raw.copy()
|
|
89
|
+
acceleration = np.zeros(shape=(1,))
|
|
90
|
+
# If both
|
|
91
|
+
else:
|
|
92
|
+
deceleration, idx_dec = get_acc_sums(acceleration_raw, dr_idx)
|
|
93
|
+
|
|
94
|
+
ar_idx = np.where(acceleration_raw >= 0.0)[0]
|
|
95
|
+
acceleration, idx_acc = get_acc_sums(acceleration_raw, ar_idx)
|
|
96
|
+
|
|
97
|
+
return acceleration_raw, acceleration, np.abs(deceleration)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def compute_jerk(speed: np.ndarray, timestamps: np.ndarray) -> np.ndarray:
|
|
101
|
+
"""Computes the jerk from the acceleration profile and time delta.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
speed (np.ndarray): The speed time series (m/s) (shape: [T,]).
|
|
105
|
+
timestamps (np.ndarray): The timestamps corresponding to each speed measurement (shape: [T,]).
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
np.ndarray or None: The jerk time series (m/s^3), or None if NaN values are present.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If speed and timestamps do not have the same shape.
|
|
112
|
+
"""
|
|
113
|
+
if not speed.shape == timestamps.shape:
|
|
114
|
+
raise ValueError("Speed and timestamps must have the same shape.")
|
|
115
|
+
|
|
116
|
+
acceleration = np.gradient(speed, timestamps)
|
|
117
|
+
jerk = np.gradient(acceleration, timestamps)
|
|
118
|
+
|
|
119
|
+
if np.isnan(jerk).any():
|
|
120
|
+
logger.warning(f"Nan value in agent jerk: {jerk}")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
return jerk
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def compute_waiting_period(
|
|
127
|
+
position: np.ndarray,
|
|
128
|
+
speed: np.ndarray,
|
|
129
|
+
timestamps: np.ndarray,
|
|
130
|
+
conflict_points: np.ndarray | None,
|
|
131
|
+
stationary_speed: float = 0.0,
|
|
132
|
+
) -> tuple[np.ndarray, ...]:
|
|
133
|
+
"""Computes the waiting period for an agent based on its position and speed.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
position (np.ndarray): The positions of the agent over time (shape: [T, 2]).
|
|
137
|
+
speed (np.ndarray): The speeds of the agent over time (shape: [T,]).
|
|
138
|
+
timestamps (np.ndarray): The timestamps corresponding to each position/speed (shape: [T,]).
|
|
139
|
+
conflict_points (np.ndarray or None): The conflict points to check against (shape: [C, 2] or None).
|
|
140
|
+
stationary_speed (float, optional): The speed threshold below which the agent is considered stationary. Defaults
|
|
141
|
+
to 0.0.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
tuple:
|
|
145
|
+
waiting_period (np.ndarray): The waiting interval over the distance to the closest conflict point at that
|
|
146
|
+
distance (shape: [N,]).
|
|
147
|
+
waiting_intervals (np.ndarray): The duration of each waiting interval (shape: [N,]).
|
|
148
|
+
waiting_distances (np.ndarray): The minimum distance to conflict points during each waiting interval
|
|
149
|
+
(shape: [N,]).
|
|
150
|
+
"""
|
|
151
|
+
waiting_intervals = np.zeros(shape=(position.shape[0]))
|
|
152
|
+
waiting_distances = np.inf * np.ones(shape=(position.shape[0]))
|
|
153
|
+
waiting_period = np.zeros(shape=(position.shape[0]))
|
|
154
|
+
if conflict_points is None or conflict_points.shape[0] == 0:
|
|
155
|
+
return waiting_period, waiting_intervals, waiting_distances
|
|
156
|
+
|
|
157
|
+
dt = timestamps[1:] - timestamps[:-1]
|
|
158
|
+
# On a per-timestep basis, this considers an agent to be waiting if its speed is less than or
|
|
159
|
+
# equal to the predefined stationary speed.
|
|
160
|
+
is_waiting = speed <= stationary_speed
|
|
161
|
+
if sum(is_waiting) > 0:
|
|
162
|
+
# Find all the transitions between moving and being stationary
|
|
163
|
+
is_waiting = np.hstack([[False], is_waiting, [False]])
|
|
164
|
+
is_waiting = np.diff(is_waiting.astype(int))
|
|
165
|
+
|
|
166
|
+
# Get all intervals where the agent is waiting
|
|
167
|
+
starts = np.where(is_waiting == 1)[0]
|
|
168
|
+
ends = np.where(is_waiting == -1)[0]
|
|
169
|
+
|
|
170
|
+
waiting_intervals = np.array([dt[start:end].sum() for start, end in zip(starts, ends)])
|
|
171
|
+
# intervals = np.array([end - start for start, end in zip(starts, ends)])
|
|
172
|
+
|
|
173
|
+
# For every timestep, get the minimum distance to the set of conflict points
|
|
174
|
+
waiting_distances = np.linalg.norm(conflict_points[:, None] - position[starts], axis=-1).min(axis=0)
|
|
175
|
+
|
|
176
|
+
# TODO:
|
|
177
|
+
# # Get the index of the longest interval. Then, get the longest interval and the distance to
|
|
178
|
+
# # the closest conflict point at that interval
|
|
179
|
+
# idx = intervals.argmax()
|
|
180
|
+
# # breakpoint()
|
|
181
|
+
# waiting_period_interval_longest = intervals[idx]
|
|
182
|
+
# waiting_period_distance_longest = dists_cps[idx] + EPS
|
|
183
|
+
|
|
184
|
+
# # Get the index of the closest conflict point for each interval. Then get the interval for
|
|
185
|
+
# # that index and the distance to that conflict point
|
|
186
|
+
# idx = dists_cps.argmin()
|
|
187
|
+
# waiting_period_interval_closest_conflict = intervals[idx]
|
|
188
|
+
# waiting_period_distance_closest_conflict = dists_cps[idx] + EPS
|
|
189
|
+
|
|
190
|
+
# waiting_intervals = np.asarray(
|
|
191
|
+
# [waiting_period_interval_longest, waiting_period_interval_closest_conflict])
|
|
192
|
+
# waiting_distances_to_conflict = np.asarray(
|
|
193
|
+
# [waiting_period_distance_longest, waiting_period_distance_closest_conflict])
|
|
194
|
+
|
|
195
|
+
waiting_period = waiting_intervals / (waiting_distances + EPS)
|
|
196
|
+
return waiting_period, waiting_intervals, waiting_distances
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from omegaconf import DictConfig
|
|
5
|
+
|
|
6
|
+
import characterization.features.interaction_utils as interaction
|
|
7
|
+
from characterization.features.base_feature import BaseFeature
|
|
8
|
+
from characterization.utils.common import EPS, InteractionStatus, get_logger
|
|
9
|
+
from characterization.utils.schemas import Scenario, ScenarioFeatures
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InteractionFeatures(BaseFeature):
|
|
15
|
+
def __init__(self, config: DictConfig) -> None:
|
|
16
|
+
"""Initializes the InteractionFeatures class.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config (DictConfig): Configuration for the feature. Expected to contain key-value pairs
|
|
20
|
+
relevant to feature computation, such as thresholds or parameters. Must include
|
|
21
|
+
'return_criteria' (str), which determines whether to return 'critical' or 'average'
|
|
22
|
+
statistics for each feature.
|
|
23
|
+
"""
|
|
24
|
+
super(InteractionFeatures, self).__init__(config)
|
|
25
|
+
|
|
26
|
+
self.return_criteria = config.get("return_criteria", "critical")
|
|
27
|
+
self.agent_i = interaction.InteractionAgent()
|
|
28
|
+
self.agent_j = interaction.InteractionAgent()
|
|
29
|
+
|
|
30
|
+
def compute(self, scenario: Scenario) -> ScenarioFeatures:
|
|
31
|
+
"""Computes pairwise interaction features for each agent pair in the scenario.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
scenario (Scenario): Scenario object containing agent positions, velocities, headings,
|
|
35
|
+
validity masks, timestamps, map conflict points, distances to conflict points,
|
|
36
|
+
and other scenario-level information.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
ScenarioFeatures: An object containing computed interaction features for each valid agent pair,
|
|
40
|
+
including separation, intersection, collision, minimum time to conflict point (mTTCP),
|
|
41
|
+
interaction status, agent pair indices, and agent pair types.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If no agent combinations are found (i.e., less than two agents in the scenario).
|
|
45
|
+
"""
|
|
46
|
+
agent_combinations = list(itertools.combinations(range(scenario.num_agents), 2))
|
|
47
|
+
if agent_combinations is None:
|
|
48
|
+
raise ValueError("No agent combinations found. Ensure that the scenario has at least two agents.")
|
|
49
|
+
|
|
50
|
+
agent_types = scenario.agent_types
|
|
51
|
+
agent_masks = scenario.agent_valid
|
|
52
|
+
agent_positions = scenario.agent_positions
|
|
53
|
+
# NOTE: this is also computed as a feature in the individual features.
|
|
54
|
+
agent_velocities = np.linalg.norm(scenario.agent_velocities, axis=-1) + EPS
|
|
55
|
+
agent_headings = scenario.agent_headings.squeeze(-1)
|
|
56
|
+
conflict_points = scenario.map_conflict_points
|
|
57
|
+
dists_to_conflict_points = scenario.agent_distances_to_conflict_points
|
|
58
|
+
|
|
59
|
+
# TODO: Figure out where's best place to get these from
|
|
60
|
+
stationary_speed = scenario.stationary_speed
|
|
61
|
+
agent_to_agent_max_distance = scenario.agent_to_agent_max_distance
|
|
62
|
+
agent_to_conflict_point_max_distance = scenario.agent_to_conflict_point_max_distance
|
|
63
|
+
agent_to_agent_distance_breach = scenario.agent_to_agent_distance_breach
|
|
64
|
+
|
|
65
|
+
# Meta information to be included in ScenarioFeatures Valid interactions will be added 'agent_pair_indeces' and
|
|
66
|
+
# 'interaction_status'
|
|
67
|
+
scenario_interaction_statuses = [InteractionStatus.UNKNOWN for _ in agent_combinations]
|
|
68
|
+
scenario_agent_pair_indeces = [(i, j) for i, j in agent_combinations]
|
|
69
|
+
scenario_agents_pair_types = [(agent_types[i], agent_types[j]) for i, j in agent_combinations]
|
|
70
|
+
|
|
71
|
+
num_interactions = len(agent_combinations)
|
|
72
|
+
# Features to be included in ScenarioFeatures
|
|
73
|
+
scenario_separations = np.full(num_interactions, np.nan, dtype=np.float32)
|
|
74
|
+
scenario_intersections = np.full(num_interactions, np.nan, dtype=np.float32)
|
|
75
|
+
scenario_collisions = np.full(num_interactions, np.nan, dtype=np.float32)
|
|
76
|
+
scenario_mttcps = np.full(num_interactions, np.nan, dtype=np.float32)
|
|
77
|
+
|
|
78
|
+
# Compute distance to conflict points
|
|
79
|
+
for n, (i, j) in enumerate(agent_combinations):
|
|
80
|
+
self.agent_i.reset()
|
|
81
|
+
self.agent_j.reset()
|
|
82
|
+
|
|
83
|
+
# There should be at leat two valid timestaps for the combined agents masks
|
|
84
|
+
mask_i, mask_j = agent_masks[i], agent_masks[j]
|
|
85
|
+
mask = np.where(mask_i & mask_j)[0]
|
|
86
|
+
if not mask.sum():
|
|
87
|
+
# No valid data for this pair of agents
|
|
88
|
+
scenario_interaction_statuses[n] = InteractionStatus.MASK_NOT_VALID
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
self.agent_i.position, self.agent_j.position = agent_positions[i][mask], agent_positions[j][mask]
|
|
92
|
+
self.agent_i.velocity, self.agent_j.velocity = agent_velocities[i][mask], agent_velocities[j][mask]
|
|
93
|
+
self.agent_i.heading, self.agent_j.heading = agent_headings[i][mask], agent_headings[j][mask]
|
|
94
|
+
self.agent_i.agent_type, self.agent_j.agent_type = agent_types[i], agent_types[j]
|
|
95
|
+
|
|
96
|
+
# type: ignore
|
|
97
|
+
if conflict_points is not None and dists_to_conflict_points is not None:
|
|
98
|
+
self.agent_j.dists_to_conflict = dists_to_conflict_points[i][mask]
|
|
99
|
+
self.agent_j.dists_to_conflict = dists_to_conflict_points[j][mask]
|
|
100
|
+
|
|
101
|
+
# Check if agents are within a valid distance threshold to compute interactions
|
|
102
|
+
separations = interaction.compute_separation(self.agent_i, self.agent_j)
|
|
103
|
+
if not np.any(separations <= agent_to_agent_max_distance):
|
|
104
|
+
scenario_interaction_statuses[n] = InteractionStatus.AGENT_DISTANCE_TOO_FAR
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Check if agents are stationary
|
|
108
|
+
self.agent_i.stationary_speed = stationary_speed
|
|
109
|
+
self.agent_j.stationary_speed = stationary_speed
|
|
110
|
+
if self.agent_i.is_stationary and self.agent_i.is_stationary:
|
|
111
|
+
scenario_interaction_statuses[n] = InteractionStatus.AGENTS_STATIONARY
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# Compute interaction features
|
|
115
|
+
# separations = interaction.compute_separation(self.agent_i, self.agent_j)
|
|
116
|
+
intersections = interaction.compute_intersections(self.agent_i, self.agent_j)
|
|
117
|
+
collisions = (separations <= agent_to_agent_distance_breach) | intersections
|
|
118
|
+
intersections = intersections.astype(np.float32)
|
|
119
|
+
collisions = collisions.astype(np.float32)
|
|
120
|
+
|
|
121
|
+
# Minimum time to conflict point (mTTCP)
|
|
122
|
+
mttcps = interaction.compute_mttcp(self.agent_i, self.agent_j, agent_to_conflict_point_max_distance)
|
|
123
|
+
|
|
124
|
+
if self.return_criteria == "critical":
|
|
125
|
+
separation = separations.min()
|
|
126
|
+
intersection = intersections.sum()
|
|
127
|
+
collision = collisions.sum()
|
|
128
|
+
mttcp = mttcps.min()
|
|
129
|
+
elif self.return_criteria == "average":
|
|
130
|
+
separation = separations.mean()
|
|
131
|
+
intersection = intersections.mean()
|
|
132
|
+
collision = collisions.mean()
|
|
133
|
+
mttcp = mttcps.mean()
|
|
134
|
+
|
|
135
|
+
# Store computed features in the state dictionary
|
|
136
|
+
scenario_separations[n] = separation
|
|
137
|
+
scenario_intersections[n] = intersection
|
|
138
|
+
scenario_collisions[n] = collision
|
|
139
|
+
scenario_mttcps[n] = mttcp
|
|
140
|
+
scenario_interaction_statuses[n] = InteractionStatus.COMPUTED_OK
|
|
141
|
+
|
|
142
|
+
return ScenarioFeatures(
|
|
143
|
+
num_agents=scenario.num_agents,
|
|
144
|
+
scenario_id=scenario.scenario_id,
|
|
145
|
+
agent_types=scenario.agent_types,
|
|
146
|
+
separation=scenario_separations,
|
|
147
|
+
intersection=scenario_intersections,
|
|
148
|
+
collision=scenario_collisions,
|
|
149
|
+
mttcp=scenario_mttcps,
|
|
150
|
+
interaction_status=scenario_interaction_statuses,
|
|
151
|
+
interaction_agent_indices=scenario_agent_pair_indeces,
|
|
152
|
+
interaction_agent_types=scenario_agents_pair_types,
|
|
153
|
+
)
|