avlite 0.4.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 (75) hide show
  1. avlite/__init__.py +19 -0
  2. avlite/__main__.py +31 -0
  3. avlite/c10_perception/__init__.py +0 -0
  4. avlite/c10_perception/c11_perception_model.py +470 -0
  5. avlite/c10_perception/c12_perception_strategy.py +176 -0
  6. avlite/c10_perception/c13_localization_strategy.py +76 -0
  7. avlite/c10_perception/c14_mapping_strategy.py +36 -0
  8. avlite/c10_perception/c15_perception_algs.py +387 -0
  9. avlite/c10_perception/c16_localization_algs.py +225 -0
  10. avlite/c10_perception/c17_mapping_algs.py +0 -0
  11. avlite/c10_perception/c18_hdmap_parser.py +512 -0
  12. avlite/c10_perception/c19_settings.py +47 -0
  13. avlite/c20_planning/__init__.py +0 -0
  14. avlite/c20_planning/c21_planning_model.py +166 -0
  15. avlite/c20_planning/c22_global_planning_strategy.py +51 -0
  16. avlite/c20_planning/c23_local_planning_strategy.py +353 -0
  17. avlite/c20_planning/c24_global_hdmap_planners.py +344 -0
  18. avlite/c20_planning/c25_global_race_planners.py +140 -0
  19. avlite/c20_planning/c26_local_path_planners.py +49 -0
  20. avlite/c20_planning/c27_local_behavioral_and_velocity_planners.py +306 -0
  21. avlite/c20_planning/c28_local_lattice_planners.py +823 -0
  22. avlite/c20_planning/c29_settings.py +50 -0
  23. avlite/c30_control/__init__.py +0 -0
  24. avlite/c30_control/c31_control_model.py +35 -0
  25. avlite/c30_control/c32_control_strategy.py +77 -0
  26. avlite/c30_control/c33_pid.py +128 -0
  27. avlite/c30_control/c34_stanley.py +140 -0
  28. avlite/c30_control/c38_control_mapping.py +31 -0
  29. avlite/c30_control/c39_settings.py +45 -0
  30. avlite/c40_execution/__init__.py +0 -0
  31. avlite/c40_execution/c41_world_bridge.py +182 -0
  32. avlite/c40_execution/c42_execution_strategy.py +330 -0
  33. avlite/c40_execution/c44_sync_executer.py +104 -0
  34. avlite/c40_execution/c45_async_threaded_executer.py +290 -0
  35. avlite/c40_execution/c46_basic_sim.py +249 -0
  36. avlite/c40_execution/c49_settings.py +66 -0
  37. avlite/c50_common/__init__.py +1 -0
  38. avlite/c50_common/c51_capabilities.py +77 -0
  39. avlite/c50_common/c52_sensor_datatypes.py +121 -0
  40. avlite/c50_common/c53_trajectory_tracker.py +856 -0
  41. avlite/c50_common/c54_collision_checking.py +185 -0
  42. avlite/c50_common/c55_fps_tracker.py +42 -0
  43. avlite/c60_apps/__init__.py +0 -0
  44. avlite/c60_apps/c61_app_strategy.py +89 -0
  45. avlite/c60_apps/c62_factory.py +365 -0
  46. avlite/c60_apps/c63_plugins.py +596 -0
  47. avlite/c60_apps/c64_settings_schema.py +251 -0
  48. avlite/c60_apps/c65_setting_utils.py +471 -0
  49. avlite/c60_apps/c68_paths.py +390 -0
  50. avlite/c60_apps/c69_settings.py +35 -0
  51. avlite/plugins/__init__.py +5 -0
  52. avlite/plugins/p60_headless_mode/__init__.py +8 -0
  53. avlite/plugins/p60_headless_mode/p61_headless.py +439 -0
  54. avlite/plugins/p60_headless_mode/settings.py +13 -0
  55. avlite/plugins/p60_setting_cli/__init__.py +8 -0
  56. avlite/plugins/p60_setting_cli/p61_setting_cli.py +222 -0
  57. avlite/plugins/p60_setting_cli/settings.py +8 -0
  58. avlite/plugins/p60_visualizer_tk/__init__.py +14 -0
  59. avlite/plugins/p60_visualizer_tk/p61_visualizer_app.py +596 -0
  60. avlite/plugins/p60_visualizer_tk/p62_setting_app.py +95 -0
  61. avlite/plugins/p60_visualizer_tk/p63_plugins_app.py +1958 -0
  62. avlite/plugins/p60_visualizer_tk/p64_setting_views.py +1316 -0
  63. avlite/plugins/p60_visualizer_tk/p65_ui_lib.py +846 -0
  64. avlite/plugins/p60_visualizer_tk/p66_plot_views.py +481 -0
  65. avlite/plugins/p60_visualizer_tk/p67_stack_views.py +922 -0
  66. avlite/plugins/p60_visualizer_tk/p68_log_view.py +511 -0
  67. avlite/plugins/p60_visualizer_tk/p69_plot_lib.py +1278 -0
  68. avlite/plugins/p60_visualizer_tk/settings.py +482 -0
  69. avlite-0.4.0.dist-info/METADATA +356 -0
  70. avlite-0.4.0.dist-info/RECORD +75 -0
  71. avlite-0.4.0.dist-info/WHEEL +5 -0
  72. avlite-0.4.0.dist-info/entry_points.txt +2 -0
  73. avlite-0.4.0.dist-info/licenses/LICENSE +21 -0
  74. avlite-0.4.0.dist-info/top_level.txt +2 -0
  75. scripts/migrate_configs.py +160 -0
avlite/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ import re
2
+ from importlib.metadata import PackageNotFoundError, version
3
+ from pathlib import Path
4
+
5
+
6
+ def _package_version() -> str:
7
+ try:
8
+ return version("avlite")
9
+ except PackageNotFoundError:
10
+ pass
11
+ pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
12
+ if pyproject.is_file():
13
+ match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject.read_text(encoding="utf-8"), re.M)
14
+ if match:
15
+ return match.group(1)
16
+ return "unknown"
17
+
18
+
19
+ __version__ = _package_version()
avlite/__main__.py ADDED
@@ -0,0 +1,31 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from avlite.c60_apps.c61_app_strategy import bootstrap_apps, register_parsers, run_app
5
+
6
+
7
+ def main(argv: list[str] | None = None) -> None:
8
+ """Main entry point for the AVLite application."""
9
+ parser = argparse.ArgumentParser(prog="avlite", description="AVLite")
10
+ sub = parser.add_subparsers(dest="command")
11
+
12
+ bootstrap_apps()
13
+ register_parsers(sub)
14
+
15
+ try:
16
+ args, unknown = parser.parse_known_args(sys.argv[1:] if argv is None else argv)
17
+ except SystemExit as exc:
18
+ if exc.code not in (0, None):
19
+ sys.stderr.write("\nError parsing arguments. Use --help for usage.\n")
20
+ raise
21
+
22
+ if unknown:
23
+ sys.stderr.write(f"Ignoring unknown arguments: {unknown}\n")
24
+
25
+ exit_code = run_app(args.command, args, unknown)
26
+ if exit_code:
27
+ sys.exit(exit_code)
28
+
29
+
30
+ if __name__ == "__main__":
31
+ main()
File without changes
@@ -0,0 +1,470 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from enum import Enum, auto
5
+ import json
6
+ import logging
7
+ import xml.etree.ElementTree as ET
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import copy
13
+ import networkx as nx
14
+ import numpy as np
15
+ from scipy.spatial import KDTree
16
+ from shapely.geometry import Polygon
17
+
18
+ from avlite.c10_perception.c19_settings import PerceptionSettings
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+ EGO_AGENT_ID: int = 0
23
+
24
+
25
+
26
+
27
+ @dataclass
28
+ class PerceptionModel:
29
+ static_obstacles: list[State] = field(default_factory=list)
30
+ agent_vehicles: list[AgentState] = field(default_factory=list)
31
+ ego_vehicle: EgoState= field(default_factory=lambda: EgoState())
32
+ max_agent_vehicles: int = field(default_factory=lambda: PerceptionSettings.c11_max_agents)
33
+
34
+ prediction: Optional[PredictionModelBase] = None
35
+
36
+ # Optional map (HDMap or RaceMap)
37
+ map: Optional[Map] = None
38
+
39
+
40
+ # Raw LiDAR points that passed segmentation + range gating (diagnostic overlay)
41
+ detection_clusters: Optional[np.ndarray] = None
42
+
43
+ def add_agent_vehicle(self, agent: AgentState) -> int: # return agent_id
44
+ if len(self.agent_vehicles) == self.max_agent_vehicles:
45
+ log.info("Max num of agent reached. Deleteing Old agents")
46
+ self.agent_vehicles = []
47
+ ids = {a.agent_id for a in self.agent_vehicles}
48
+ agent.agent_id = next(i for i in range(1, len(ids) + 2) if i not in ids)
49
+ self.agent_vehicles.append(agent)
50
+ log.info(f"Total agent vehicles {len(self.agent_vehicles)}")
51
+
52
+ return agent.agent_id
53
+
54
+ def reset(self):
55
+ self.static_obstacles = []
56
+ self.agent_vehicles = []
57
+ self.prediction = None
58
+
59
+
60
+ @dataclass
61
+ class PredictionModelBase:
62
+ """Shared metadata for prediction outputs on ``PerceptionModel.prediction``."""
63
+
64
+ # Seconds between consecutive forecast samples (t, t+dt, …).
65
+ predict_delta_t: float = field(
66
+ default_factory=lambda: PerceptionSettings.c11_predict_delta_t
67
+ )
68
+
69
+
70
+ @dataclass
71
+ class SingleTrajectory(PredictionModelBase):
72
+ """Deterministic (x, y) polyline per agent."""
73
+
74
+ # agent_id -> [n_steps, 2] world x,y [m]; step k at (k+1) * predict_delta_t.
75
+ trajectories: dict[int, np.ndarray] = field(default_factory=dict)
76
+
77
+
78
+ @dataclass
79
+ class GP(PredictionModelBase):
80
+ """Gaussian-process forecast per agent (mean + joint covariance)."""
81
+
82
+ # agent_id -> [n_steps, 2] predictive mean trajectory.
83
+ means: dict[int, np.ndarray] = field(default_factory=dict)
84
+ # agent_id -> [2*n_steps, 2*n_steps] joint covariance; state order [x0,y0,x1,y1,...].
85
+ covariance: dict[int, np.ndarray] = field(default_factory=dict)
86
+
87
+
88
+ @dataclass
89
+ class GMM(PredictionModelBase):
90
+ """Gaussian-mixture multi-modal forecast per agent."""
91
+
92
+ # agent_id -> [n_modes, n_steps, 2] mode means.
93
+ trajectories: dict[int, np.ndarray] = field(default_factory=dict)
94
+ # agent_id -> [n_modes] mode weights (sum ≈ 1).
95
+ weights: dict[int, np.ndarray] = field(default_factory=dict)
96
+ # agent_id -> [n_modes, n_steps, 2, 2] position covariance per mode/step.
97
+ covariances: dict[int, np.ndarray] = field(default_factory=dict)
98
+
99
+
100
+ @dataclass
101
+ class OccupancyFlow(PredictionModelBase):
102
+ """Per-agent occupancy grid sequences."""
103
+
104
+ # agent_id -> n_steps grids, each [grid_size, grid_size].
105
+ occupancy_flow: dict[int, list[np.ndarray]] = field(default_factory=dict)
106
+ grid_bounds: dict[str, float] = field(default_factory=dict)
107
+ grid_size: int = field(default_factory=lambda: PerceptionSettings.c11_prediction_grid_size)
108
+
109
+
110
+ @dataclass
111
+ class AggregatedOccupancyFlow(PredictionModelBase):
112
+ """Lump-sum occupancy grids for all agents combined."""
113
+
114
+ occupancy_flow: list[np.ndarray] = field(default_factory=list)
115
+ grid_bounds: dict[str, float] = field(default_factory=dict)
116
+ grid_size: int = field(default_factory=lambda: PerceptionSettings.c11_prediction_grid_size)
117
+
118
+
119
+ @dataclass
120
+ class State:
121
+ x: float = 0.0
122
+ y: float = 0.0
123
+ z: float = 0.0
124
+ theta: float = PerceptionSettings.c11_state_default_heading
125
+ width: float = 2.0
126
+ length: float = 4.5
127
+
128
+ def __post_init__(self):
129
+ # initial x,y position, useful for reset
130
+ self.__init_x = self.x
131
+ self.__init_y = self.y
132
+ self.__init_theta = self.theta
133
+
134
+
135
+ def get_bb_corners(self) -> np.ndarray:
136
+ """Get the bounding box corners of the vehicle in world coordinates."""
137
+ corners_x = np.array(
138
+ [
139
+ -self.length / 2,
140
+ +self.length / 2,
141
+ +self.length / 2,
142
+ -self.length / 2,
143
+ ]
144
+ )
145
+ corners_y = np.array(
146
+ [
147
+ -self.width / 2,
148
+ -self.width / 2,
149
+ +self.width / 2,
150
+ +self.width / 2,
151
+ ]
152
+ )
153
+
154
+ rotation_matrix = np.array(
155
+ [
156
+ [np.cos(self.theta), -np.sin(self.theta)],
157
+ [np.sin(self.theta), np.cos(self.theta)],
158
+ ]
159
+ )
160
+ rotated_corners = np.dot(rotation_matrix, np.array([corners_x, corners_y]))
161
+
162
+ rotated_corners_x = rotated_corners[0, :] + self.x
163
+ rotated_corners_y = rotated_corners[1, :] + self.y
164
+
165
+ return np.c_[rotated_corners_x, rotated_corners_y]
166
+
167
+ def reset(self):
168
+ self.x = self.__init_x
169
+ self.y = self.__init_y
170
+ self.theta = self.__init_theta
171
+
172
+ def get_copy(self):
173
+ return copy.deepcopy(self)
174
+
175
+ def get_bb_polygon(self):
176
+ return Polygon(self.get_bb_corners())
177
+
178
+
179
+ class AgentType(Enum):
180
+ ACKERMANN = auto()
181
+ DIFF_DRIVE = auto()
182
+ AERIAL = auto()
183
+ SURFACE_VESSEL = auto() # boats, USVs — water surface
184
+ UNDERWATER = auto() # AUVs — subsurface
185
+ CYCLIST = auto()
186
+ PEDESTRIAN = auto()
187
+ DYNAMIC_OBJECT = auto()
188
+
189
+ @dataclass
190
+ class AgentState(State):
191
+ velocity: float = 0.0
192
+ agent_id: int = -1
193
+ agent_type: AgentType = AgentType.ACKERMANN
194
+
195
+ def __post_init__(self):
196
+ super().__post_init__()
197
+ self.__init_speed = self.velocity
198
+
199
+ def reset(self):
200
+ super().reset()
201
+ self.velocity = self.__init_speed
202
+
203
+
204
+
205
+ @dataclass
206
+ class EgoState(AgentState):
207
+ """Ego vehicle state with additional properties in future."""
208
+ agent_id: int = EGO_AGENT_ID
209
+ agent_type: AgentType = AgentType.ACKERMANN
210
+
211
+
212
+ class Map(ABC):
213
+ """Static world geometry with an optional WGS84 reference point."""
214
+
215
+ source_path: str
216
+
217
+ @property
218
+ @abstractmethod
219
+ def reference_point(self) -> tuple[float, float] | None:
220
+ """WGS84 (lat_deg, lon_deg) when available."""
221
+
222
+ @staticmethod
223
+ @abstractmethod
224
+ def is_loadable(path: Path | str) -> bool:
225
+ """Return True when *path* is a supported map file."""
226
+
227
+ @classmethod
228
+ @abstractmethod
229
+ def from_path(cls, path: Path | str) -> Map:
230
+ """Load a map instance from *path*."""
231
+
232
+ @staticmethod
233
+ def open(path: Path | str) -> Map | None:
234
+ """Dispatch to ``HDMap`` or ``RaceMap`` based on file format."""
235
+ path = Path(path)
236
+ if HDMap.is_loadable(path):
237
+ return HDMap.from_path(path)
238
+ if RaceMap.is_loadable(path):
239
+ return RaceMap.from_path(path)
240
+ return None
241
+
242
+
243
+ @dataclass
244
+ class RaceMap(Map):
245
+ """Race corridor map from left/right boundary JSON."""
246
+
247
+ source_path: str
248
+ left_bound: np.ndarray = field(default_factory=lambda: np.array([]))
249
+ right_bound: np.ndarray = field(default_factory=lambda: np.array([]))
250
+ _reference_point: tuple[float, float] | None = None
251
+
252
+ @property
253
+ def reference_point(self) -> tuple[float, float] | None:
254
+ return self._reference_point
255
+
256
+ @staticmethod
257
+ def is_loadable(path: Path | str) -> bool:
258
+ """True when *path* is a race-boundary JSON with bounds and ReferencePoint."""
259
+ path = Path(path)
260
+ if path.suffix.lower() != ".json" or not path.is_file():
261
+ return False
262
+ try:
263
+ with path.open(encoding="utf-8") as f:
264
+ data = json.load(f)
265
+ except (OSError, json.JSONDecodeError):
266
+ return False
267
+ if not all(k in data for k in ("LeftBound", "RightBound", "ReferencePoint")):
268
+ return False
269
+ left = data["LeftBound"]
270
+ right = data["RightBound"]
271
+ if not left or not right:
272
+ return False
273
+ if not isinstance(left[0], list) or not isinstance(right[0], list):
274
+ return False
275
+ ref = data["ReferencePoint"]
276
+ if not isinstance(ref, list) or len(ref) < 2:
277
+ return False
278
+ try:
279
+ float(ref[0])
280
+ float(ref[1])
281
+ except (TypeError, ValueError):
282
+ return False
283
+ return True
284
+
285
+ @classmethod
286
+ def from_path(cls, path: Path | str) -> RaceMap:
287
+ path = Path(path)
288
+ with path.open(encoding="utf-8") as f:
289
+ data = json.load(f)
290
+ left = np.array(data["LeftBound"])[:, :2]
291
+ right = np.array(data["RightBound"])[:, :2]
292
+ ref = data["ReferencePoint"]
293
+ ref_pt = (float(ref[0]), float(ref[1]))
294
+ return cls(source_path=str(path), left_bound=left, right_bound=right, _reference_point=ref_pt,)
295
+
296
+
297
+ @dataclass
298
+ class HDMap(Map):
299
+ """Compact HD map representation for global planning."""
300
+
301
+ @dataclass
302
+ class Lane:
303
+ id: int
304
+ uid: str
305
+ lane_element: ET.Element
306
+ center_line: np.ndarray = field(default_factory=lambda: np.array([]))
307
+ left_d: list[float] = field(default_factory=list)
308
+ right_d: list[float] = field(default_factory=list)
309
+ road: Optional["HDMap.Road"] = None
310
+ pred_id: str = ""
311
+ succ_id: str = ""
312
+ pred_type: str = "lane"
313
+ succ_type: str = "lane"
314
+ side: str = "left"
315
+ type: str = "driving"
316
+ road_id: str = ""
317
+ width: float = 0.0
318
+ lane_section_idx: int = 0
319
+ predecessors: list["HDMap.Lane"] = field(default_factory=list)
320
+ successors: list["HDMap.Lane"] = field(default_factory=list)
321
+ neighbors: set["HDMap.Lane"] = field(default_factory=set)
322
+ drivable_neighbors: set["HDMap.Lane"] = field(default_factory=set)
323
+
324
+ def __hash__(self):
325
+ return hash(self.uid)
326
+
327
+ @dataclass
328
+ class Road:
329
+ """Compact road representation for global planning."""
330
+
331
+ id: str
332
+ road_element: ET.Element
333
+ pred_id: str = ""
334
+ succ_id: str = ""
335
+ pred_type: str = "road"
336
+ succ_type: str = "road"
337
+ length: float = 0.0
338
+ junction_id: str = ""
339
+ center_line: np.ndarray = field(default_factory=lambda: np.array([]))
340
+ predecessors: list["HDMap.Road"] = field(default_factory=list)
341
+ successors: list["HDMap.Road"] = field(default_factory=list)
342
+ lane_sections: list[list["HDMap.Lane"]] = field(default_factory=list)
343
+ lane_section_s_vals: list[float] = field(default_factory=list)
344
+ reversed: bool = False
345
+
346
+ xodr_file_name: str = ""
347
+ sampling_resolution: float = 0.1
348
+ roads: list[Road] = field(default_factory=list)
349
+ lanes: list[Lane] = field(default_factory=list)
350
+ road_by_id: dict[str, Road] = field(default_factory=dict)
351
+ lane_by_uid: dict[str, Lane] = field(default_factory=dict)
352
+ junction_by_id: dict[str, list[Road]] = field(default_factory=dict)
353
+ road_network: nx.DiGraph = field(default_factory=nx.DiGraph)
354
+ lane_network: nx.DiGraph = field(default_factory=nx.DiGraph)
355
+ root: ET.Element | None = field(default=None, repr=False)
356
+
357
+ _point_to_road: dict[tuple[float, float], Road] = field(default_factory=dict, init=False, repr=False)
358
+ _point_to_drivable_lane: dict[tuple[float, float], Lane] = field(default_factory=dict, init=False, repr=False)
359
+ _road_kdtree: Optional[KDTree] = field(default=None, init=False, repr=False)
360
+ _lane_kdtree_drivable: Optional[KDTree] = field(default=None, init=False, repr=False)
361
+ _all_road_points: list[tuple[float, float]] = field(default_factory=list, init=False, repr=False)
362
+ _all_drivable_lane_points: list[tuple[float, float]] = field(default_factory=list, init=False, repr=False)
363
+ _reference_point: tuple[float, float] | None = field(default=None, init=False, repr=False)
364
+
365
+ @property
366
+ def source_path(self) -> str:
367
+ return self.xodr_file_name
368
+
369
+ @property
370
+ def reference_point(self) -> tuple[float, float] | None:
371
+ return self._reference_point
372
+
373
+ @staticmethod
374
+ def is_loadable(path: Path | str) -> bool:
375
+ path = Path(path)
376
+ return path.suffix.lower() == ".xodr" and path.is_file()
377
+
378
+ @classmethod
379
+ def from_path(cls, path: Path | str) -> HDMap:
380
+ return cls(xodr_file_name=str(Path(path).resolve()))
381
+
382
+ def __post_init__(self) -> None:
383
+ if not self.xodr_file_name:
384
+ log.error("No OpenDRIVE file specified.")
385
+ return
386
+ from avlite.c10_perception.c18_hdmap_parser import parse_opendrive
387
+
388
+ parse_opendrive(self)
389
+
390
+ def find_nearest_road(self, x: float, y: float) -> Road | None:
391
+ if self._road_kdtree is not None:
392
+ _, index = self._road_kdtree.query((x, y))
393
+ if index >= 0 and index < len(self._all_road_points):
394
+ px, py = self._all_road_points[index]
395
+ if (px, py) not in self._point_to_road:
396
+ log.error("Point not found in point_to_road mapping: (%s, %s)", px, py)
397
+ return self._point_to_road.get((px, py), None)
398
+
399
+ def find_nearest_lane(self, x: float, y: float) -> Lane | None:
400
+ if self._lane_kdtree_drivable is not None:
401
+ _, index = self._lane_kdtree_drivable.query((x, y))
402
+ if 0 <= index < len(self._all_drivable_lane_points):
403
+ lx, ly = self._all_drivable_lane_points[index]
404
+ if (lx, ly) not in self._point_to_drivable_lane:
405
+ log.error("Point not found in point_to_lane mapping: (%s, %s)", x, y)
406
+ return self._point_to_drivable_lane.get((lx, ly), None)
407
+
408
+ def find_nearest_lane_and_idx(self, x: float, y: float) -> tuple[Lane | None, int]:
409
+ lane = self.find_nearest_lane(x, y)
410
+ if lane is None or lane.center_line.size == 0:
411
+ return None, -1
412
+ dists = np.linalg.norm(lane.center_line - np.array([[x], [y]]), axis=0)
413
+ idx = int(np.argmin(dists))
414
+ return lane, idx
415
+
416
+ def can_laneA_access_laneB(self, lane_a: Lane, lane_b: Lane) -> bool:
417
+ check1 = lane_b in lane_a.neighbors
418
+ b_start_end = [lane_b.center_line[:, 0], lane_b.center_line[:, -1]]
419
+ a = lane_a.center_line[:, -1] if int(lane_a.id) < 0 else lane_a.center_line[:, 0]
420
+ dists = [(np.linalg.norm(a - b), j) for j, b in enumerate(b_start_end)]
421
+ min_dist, b_idx = min(dists, key=lambda item: item[0])
422
+ check2 = min_dist < 0.5
423
+ check3 = False
424
+ if check2:
425
+ if int(lane_a.id) < 0:
426
+ vec_a = lane_a.center_line[:, -3] - lane_a.center_line[:, -1]
427
+ else:
428
+ vec_a = lane_a.center_line[:, 2] - lane_a.center_line[:, 0]
429
+ if int(lane_b.id) < 0:
430
+ vec_b = (
431
+ lane_b.center_line[:, 0] - lane_b.center_line[:, 2]
432
+ if b_idx == 0
433
+ else lane_b.center_line[:, -1] - lane_b.center_line[:, -3]
434
+ )
435
+ else:
436
+ vec_b = (
437
+ lane_b.center_line[:, 1] - lane_b.center_line[:, 0]
438
+ if b_idx == 0
439
+ else lane_b.center_line[:, -1] - lane_b.center_line[:, -3]
440
+ )
441
+ norm_a = np.linalg.norm(vec_a)
442
+ norm_b = np.linalg.norm(vec_b)
443
+ if norm_a > 0 and norm_b > 0:
444
+ check3 = np.dot(vec_a / norm_a, vec_b / norm_b) > 0.9
445
+ return check1 and check2 and check3
446
+
447
+ def road_has_driving_lanes(self, road: Road) -> bool:
448
+ road_element = road.road_element
449
+ for section in road_element.findall(".//laneSection"):
450
+ for lane in section.findall(".//lane"):
451
+ if lane.get("type") == "driving":
452
+ return True
453
+ return False
454
+
455
+ def road_is_bidirectional(self, road: Road) -> bool:
456
+ road_element = road.road_element
457
+ right = False
458
+ left = False
459
+ for section in road_element.findall(".//laneSection"):
460
+ for lane in section.findall(".//lane"):
461
+ lane_type = lane.get("type")
462
+ lane_id = int(lane.get("id", "0"))
463
+ if lane_type == "driving" and lane_id < 0:
464
+ right = True
465
+ if lane_type == "driving" and lane_id > 0:
466
+ left = True
467
+ if right and left:
468
+ return True
469
+ return False
470
+
@@ -0,0 +1,176 @@
1
+ import logging
2
+ from abc import ABC, abstractmethod
3
+ from avlite.c10_perception.c11_perception_model import PerceptionModel
4
+ from avlite.c10_perception.c19_settings import PerceptionSettings, PerceptionSettingsSchema
5
+ from avlite.c50_common.c51_capabilities import WorldCapability, StackCapability
6
+ from avlite.c50_common.c52_sensor_datatypes import SensorFrame
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ class PerceptionStrategy(ABC):
12
+ """
13
+ Abstract base class for perception strategies.
14
+ This class defines the interface for perception strategies, including methods for detection, tracking, and prediction
15
+ """
16
+ registry = {}
17
+ def __init__(self, perception_model: PerceptionModel, setting: PerceptionSettingsSchema = PerceptionSettings):
18
+ self.perception_model = perception_model
19
+
20
+ @property
21
+ @abstractmethod
22
+ def world_requirements(self) -> set[WorldCapability]:
23
+ """World (sensor) capabilities this strategy requires from the bridge."""
24
+ pass
25
+
26
+ @property
27
+ def stack_requirements(self) -> set[StackCapability]:
28
+ """Upstream stack capabilities this strategy depends on (default: none)."""
29
+ return set()
30
+
31
+ @property
32
+ @abstractmethod
33
+ def stack_capabilities(self) -> set[StackCapability]:
34
+ """Stack capabilities this strategy provides to downstream modules."""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def perceive(
39
+ self,
40
+ perception_model: PerceptionModel | None = None,
41
+ sensors: SensorFrame | None = None,
42
+ ) -> PerceptionModel | None:
43
+ """Main perception method that combines detection, tracking, and prediction."""
44
+ raise NotImplementedError("Perception method not implemented.")
45
+
46
+ def reset(self):
47
+ """
48
+ Reset the perception strategy to its initial state.
49
+ """
50
+ pass
51
+
52
+ def __init_subclass__(cls, abstract=False, **kwargs):
53
+ super().__init_subclass__(**kwargs)
54
+ if not abstract:
55
+ PerceptionStrategy.registry[cls.__name__] = cls
56
+
57
+
58
+ class DetectionStrategy(ABC):
59
+ """
60
+ A simple perception strategy that only performs detection.
61
+ """
62
+ registry = {}
63
+
64
+ @property
65
+ @abstractmethod
66
+ def world_requirements(self) -> set[WorldCapability]:
67
+ pass
68
+
69
+ @abstractmethod
70
+ def detect(self, perception_model: PerceptionModel, rgb_img=None, depth_img=None, lidar_data=None) -> PerceptionModel:
71
+ """
72
+ Detect objects in the environment using the specified detection method.
73
+ """
74
+ pass
75
+
76
+ def __init_subclass__(cls, abstract=False, **kwargs):
77
+ super().__init_subclass__(**kwargs)
78
+ if not abstract:
79
+ DetectionStrategy.registry[cls.__name__] = cls
80
+
81
+
82
+ class TrackingStrategy(ABC):
83
+ registry = {}
84
+
85
+ @property
86
+ @abstractmethod
87
+ def world_requirements(self) -> set[WorldCapability]:
88
+ pass
89
+
90
+ @abstractmethod
91
+ def track(self, perception_model: PerceptionModel) -> PerceptionModel:
92
+ pass
93
+
94
+ def __init_subclass__(cls, abstract=False, **kwargs):
95
+ super().__init_subclass__(**kwargs)
96
+ if not abstract:
97
+ TrackingStrategy.registry[cls.__name__] = cls
98
+
99
+
100
+ class PredictionStrategy(ABC):
101
+ registry = {}
102
+
103
+ @property
104
+ @abstractmethod
105
+ def world_requirements(self) -> set[WorldCapability]:
106
+ pass
107
+
108
+ @abstractmethod
109
+ def predict(self, perception_model: PerceptionModel) -> PerceptionModel | None:
110
+ pass
111
+
112
+ def __init_subclass__(cls, abstract=False, **kwargs):
113
+ super().__init_subclass__(**kwargs)
114
+ if not abstract:
115
+ PredictionStrategy.registry[cls.__name__] = cls
116
+
117
+
118
+ class PerceptionPipeline(PerceptionStrategy):
119
+ """
120
+ Pipelined perception strategy: detect → track → predict.
121
+ Each stage is resolved by name from its registry at construction time.
122
+ Empty name means that stage is skipped (ground truth for detect/track; no prediction).
123
+ """
124
+ def __init__(self, perception_model: PerceptionModel, setting: PerceptionSettingsSchema = PerceptionSettings):
125
+ super().__init__(perception_model, setting)
126
+ self._detector = self._resolve(DetectionStrategy.registry, setting.c12_detection_strategy)
127
+ self._tracker = self._resolve(TrackingStrategy.registry, setting.c12_tracking_strategy)
128
+ self._predictor = self._resolve(PredictionStrategy.registry, setting.c12_prediction_strategy)
129
+
130
+ @staticmethod
131
+ def _resolve(registry: dict, name: str):
132
+ if name and name in registry:
133
+ return registry[name]()
134
+ return None
135
+
136
+ @property
137
+ def world_requirements(self) -> set[WorldCapability]:
138
+ reqs = set()
139
+ for child in (self._detector, self._tracker, self._predictor):
140
+ if child is not None:
141
+ reqs |= child.world_requirements
142
+ return reqs
143
+
144
+ @property
145
+ def stack_requirements(self) -> set[StackCapability]:
146
+ # Stages with no strategy fall back to ground truth from the world bridge
147
+ reqs = set()
148
+ if self._detector is None:
149
+ reqs.add(StackCapability.DETECTION)
150
+ if self._tracker is None:
151
+ reqs.add(StackCapability.TRACKING)
152
+ return reqs
153
+
154
+ @property
155
+ def stack_capabilities(self) -> set[StackCapability]:
156
+ return {StackCapability.DETECTION, StackCapability.TRACKING, StackCapability.PREDICTION}
157
+
158
+ def perceive(
159
+ self,
160
+ perception_model=None,
161
+ sensors: SensorFrame | None = None,
162
+ ) -> PerceptionModel | None:
163
+ if perception_model is not None:
164
+ self.perception_model = perception_model
165
+ if self._detector is not None:
166
+ self.perception_model = self._detector.detect(
167
+ self.perception_model,
168
+ rgb_img=sensors.rgb if sensors else None,
169
+ depth_img=sensors.depth if sensors else None,
170
+ lidar_data=sensors.lidar if sensors else None,
171
+ )
172
+ if self._tracker is not None:
173
+ self.perception_model = self._tracker.track(self.perception_model)
174
+ if self._predictor is not None:
175
+ self.perception_model = self._predictor.predict(self.perception_model)
176
+ return self.perception_model