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.
Files changed (33) hide show
  1. characterization/__init__.py +0 -0
  2. characterization/features/__init__.py +14 -0
  3. characterization/features/base_feature.py +45 -0
  4. characterization/features/individual_features.py +158 -0
  5. characterization/features/individual_utils.py +196 -0
  6. characterization/features/interaction_features.py +153 -0
  7. characterization/features/interaction_utils.py +252 -0
  8. characterization/probing/__init__.py +0 -0
  9. characterization/processors/__init__.py +0 -0
  10. characterization/processors/base_processor.py +78 -0
  11. characterization/processors/feature_processor.py +61 -0
  12. characterization/processors/scores_processor.py +92 -0
  13. characterization/run_processor.py +48 -0
  14. characterization/scorer/__init__.py +1 -0
  15. characterization/scorer/base_scorer.py +82 -0
  16. characterization/scorer/individual_scorer.py +97 -0
  17. characterization/scorer/interaction_scorer.py +88 -0
  18. characterization/utils/__init__.py +0 -0
  19. characterization/utils/common.py +130 -0
  20. characterization/utils/datasets/__init__.py +0 -0
  21. characterization/utils/datasets/dataset.py +155 -0
  22. characterization/utils/datasets/waymo.py +380 -0
  23. characterization/utils/datasets/waymo_preprocess.py +412 -0
  24. characterization/utils/schemas.py +140 -0
  25. characterization/utils/viz/__init__.py +0 -0
  26. characterization/utils/viz/visualizer.py +73 -0
  27. characterization/utils/viz/waymo.py +768 -0
  28. characterization/viz_scores_pdf.py +201 -0
  29. scenario_characterization-0.1.0.dist-info/LICENSE +201 -0
  30. scenario_characterization-0.1.0.dist-info/METADATA +288 -0
  31. scenario_characterization-0.1.0.dist-info/RECORD +33 -0
  32. scenario_characterization-0.1.0.dist-info/WHEEL +5 -0
  33. scenario_characterization-0.1.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,14 @@
1
+ SUPPORTED_FEATURES = [
2
+ "random_feature",
3
+ "speed",
4
+ "acceleration",
5
+ "deceleration",
6
+ "jerk",
7
+ "waiting_period",
8
+ "waiting_interval",
9
+ "waiting_distance",
10
+ "separation",
11
+ "intersection",
12
+ "collision",
13
+ "mttcp",
14
+ ]
@@ -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
+ )