simple-autonomous-car 0.1.2__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 (50) hide show
  1. simple_autonomous_car/__init__.py +96 -0
  2. simple_autonomous_car/alerts/__init__.py +5 -0
  3. simple_autonomous_car/alerts/track_bounds_alert.py +276 -0
  4. simple_autonomous_car/car/__init__.py +5 -0
  5. simple_autonomous_car/car/car.py +234 -0
  6. simple_autonomous_car/constants.py +112 -0
  7. simple_autonomous_car/control/__init__.py +7 -0
  8. simple_autonomous_car/control/base_controller.py +152 -0
  9. simple_autonomous_car/control/controller_viz.py +282 -0
  10. simple_autonomous_car/control/pid_controller.py +153 -0
  11. simple_autonomous_car/control/pure_pursuit_controller.py +578 -0
  12. simple_autonomous_car/costmap/__init__.py +12 -0
  13. simple_autonomous_car/costmap/base_costmap.py +187 -0
  14. simple_autonomous_car/costmap/grid_costmap.py +507 -0
  15. simple_autonomous_car/costmap/inflation.py +126 -0
  16. simple_autonomous_car/detection/__init__.py +5 -0
  17. simple_autonomous_car/detection/error_detector.py +165 -0
  18. simple_autonomous_car/filters/__init__.py +7 -0
  19. simple_autonomous_car/filters/base_filter.py +119 -0
  20. simple_autonomous_car/filters/kalman_filter.py +131 -0
  21. simple_autonomous_car/filters/particle_filter.py +162 -0
  22. simple_autonomous_car/footprint/__init__.py +7 -0
  23. simple_autonomous_car/footprint/base_footprint.py +128 -0
  24. simple_autonomous_car/footprint/circular_footprint.py +73 -0
  25. simple_autonomous_car/footprint/rectangular_footprint.py +123 -0
  26. simple_autonomous_car/frames/__init__.py +21 -0
  27. simple_autonomous_car/frames/frenet.py +267 -0
  28. simple_autonomous_car/maps/__init__.py +9 -0
  29. simple_autonomous_car/maps/frenet_map.py +97 -0
  30. simple_autonomous_car/maps/grid_ground_truth_map.py +83 -0
  31. simple_autonomous_car/maps/grid_map.py +361 -0
  32. simple_autonomous_car/maps/ground_truth_map.py +64 -0
  33. simple_autonomous_car/maps/perceived_map.py +169 -0
  34. simple_autonomous_car/perception/__init__.py +5 -0
  35. simple_autonomous_car/perception/perception.py +107 -0
  36. simple_autonomous_car/planning/__init__.py +7 -0
  37. simple_autonomous_car/planning/base_planner.py +184 -0
  38. simple_autonomous_car/planning/goal_planner.py +261 -0
  39. simple_autonomous_car/planning/track_planner.py +199 -0
  40. simple_autonomous_car/sensors/__init__.py +6 -0
  41. simple_autonomous_car/sensors/base_sensor.py +105 -0
  42. simple_autonomous_car/sensors/lidar_sensor.py +145 -0
  43. simple_autonomous_car/track/__init__.py +5 -0
  44. simple_autonomous_car/track/track.py +463 -0
  45. simple_autonomous_car/visualization/__init__.py +25 -0
  46. simple_autonomous_car/visualization/alert_viz.py +316 -0
  47. simple_autonomous_car/visualization/utils.py +169 -0
  48. simple_autonomous_car-0.1.2.dist-info/METADATA +324 -0
  49. simple_autonomous_car-0.1.2.dist-info/RECORD +50 -0
  50. simple_autonomous_car-0.1.2.dist-info/WHEEL +4 -0
@@ -0,0 +1,162 @@
1
+ """Particle filter for state estimation."""
2
+
3
+ import numpy as np
4
+
5
+ from simple_autonomous_car.filters.base_filter import BaseFilter
6
+
7
+
8
+ class ParticleFilter(BaseFilter):
9
+ """
10
+ Particle filter for state estimation.
11
+
12
+ Uses a set of particles to represent the probability distribution
13
+ of the state. Good for non-linear systems and multi-modal distributions.
14
+
15
+ Parameters
16
+ ----------
17
+ initial_state : np.ndarray
18
+ Initial state vector (e.g., [x, y]).
19
+ initial_covariance : np.ndarray
20
+ Initial covariance matrix (used to initialize particles).
21
+ num_particles : int, default=100
22
+ Number of particles.
23
+ process_noise : np.ndarray
24
+ Process noise covariance matrix.
25
+ measurement_noise : np.ndarray
26
+ Measurement noise covariance matrix.
27
+ name : str, default="particle_filter"
28
+ Filter name.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ initial_state: np.ndarray,
34
+ initial_covariance: np.ndarray,
35
+ num_particles: int = 100,
36
+ process_noise: np.ndarray | None = None,
37
+ measurement_noise: np.ndarray | None = None,
38
+ name: str = "particle_filter",
39
+ ):
40
+ super().__init__(name=name)
41
+ self.state_dim = len(initial_state)
42
+ self.num_particles = num_particles
43
+ self.process_noise = (
44
+ np.asarray(process_noise, dtype=np.float64)
45
+ if process_noise is not None
46
+ else np.eye(self.state_dim) * 0.1
47
+ )
48
+ self.measurement_noise = (
49
+ np.asarray(measurement_noise, dtype=np.float64)
50
+ if measurement_noise is not None
51
+ else np.eye(self.state_dim) * 0.1
52
+ )
53
+
54
+ # Initialize particles
55
+ self.particles = np.random.multivariate_normal(
56
+ initial_state, initial_covariance, num_particles
57
+ )
58
+ self.weights = np.ones(num_particles) / num_particles
59
+
60
+ def predict(self, dt: float, control: dict[str, float] | None = None) -> None:
61
+ """
62
+ Predict state forward in time.
63
+
64
+ Parameters
65
+ ----------
66
+ dt : float
67
+ Time step in seconds.
68
+ control : dict, optional
69
+ Control inputs (e.g., {"velocity": 5.0, "heading": 0.5}).
70
+ """
71
+ if not self.enabled:
72
+ return
73
+
74
+ # Add process noise to particles
75
+ noise = np.random.multivariate_normal(
76
+ np.zeros(self.state_dim), self.process_noise * dt, self.num_particles
77
+ )
78
+ self.particles += noise
79
+
80
+ # Apply control if provided (simple constant velocity model)
81
+ if control is not None:
82
+ if "velocity" in control and "heading" in control:
83
+ v = control["velocity"]
84
+ h = control["heading"]
85
+ self.particles[:, 0] += v * np.cos(h) * dt # x
86
+ self.particles[:, 1] += v * np.sin(h) * dt # y
87
+
88
+ def update(
89
+ self, measurement: np.ndarray, measurement_covariance: np.ndarray | None = None
90
+ ) -> None:
91
+ """
92
+ Update state estimate with measurement.
93
+
94
+ Parameters
95
+ ----------
96
+ measurement : np.ndarray
97
+ Measurement vector.
98
+ measurement_covariance : np.ndarray, optional
99
+ Measurement noise covariance (uses default if not provided).
100
+ """
101
+ if not self.enabled:
102
+ return
103
+
104
+ measurement = np.asarray(measurement, dtype=np.float64)
105
+ r_matrix = (
106
+ measurement_covariance if measurement_covariance is not None else self.measurement_noise
107
+ )
108
+
109
+ # Update weights based on measurement likelihood
110
+ for i in range(self.num_particles):
111
+ # Compute likelihood: p(measurement | particle)
112
+ residual = measurement - self.particles[i, : len(measurement)]
113
+ likelihood = np.exp(-0.5 * residual.T @ np.linalg.inv(r_matrix) @ residual)
114
+ self.weights[i] *= likelihood
115
+
116
+ # Normalize weights
117
+ self.weights /= np.sum(self.weights)
118
+
119
+ # Resample if effective number of particles is too low
120
+ effective_particles = 1.0 / np.sum(self.weights**2)
121
+ if effective_particles < self.num_particles / 2:
122
+ self._resample()
123
+
124
+ def _resample(self) -> None:
125
+ """Resample particles based on weights."""
126
+ # Systematic resampling
127
+ cumulative_weights = np.cumsum(self.weights)
128
+ step = 1.0 / self.num_particles
129
+ u = np.random.uniform(0, step)
130
+
131
+ new_particles = np.zeros_like(self.particles)
132
+ j = 0
133
+ for i in range(self.num_particles):
134
+ while u > cumulative_weights[j]:
135
+ j += 1
136
+ new_particles[i] = self.particles[j]
137
+ u += step
138
+
139
+ self.particles = new_particles
140
+ self.weights = np.ones(self.num_particles) / self.num_particles
141
+
142
+ def get_state(self) -> np.ndarray:
143
+ """Get current state estimate (weighted mean of particles)."""
144
+ result = np.average(self.particles, axis=0, weights=self.weights)
145
+ return np.asarray(result, dtype=np.float64)
146
+
147
+ def get_covariance(self) -> np.ndarray:
148
+ """Get current state covariance."""
149
+ state = self.get_state()
150
+ diff = self.particles - state
151
+ return np.cov(diff.T, aweights=self.weights)
152
+
153
+ def reset(
154
+ self, initial_state: np.ndarray, initial_covariance: np.ndarray | None = None
155
+ ) -> None:
156
+ """Reset filter to initial state."""
157
+ if initial_covariance is None:
158
+ initial_covariance = np.eye(self.state_dim) * 0.1
159
+ self.particles = np.random.multivariate_normal(
160
+ initial_state, initial_covariance, self.num_particles
161
+ )
162
+ self.weights = np.ones(self.num_particles) / self.num_particles
@@ -0,0 +1,7 @@
1
+ """Footprint module for vehicle shape representation."""
2
+
3
+ from simple_autonomous_car.footprint.base_footprint import BaseFootprint
4
+ from simple_autonomous_car.footprint.circular_footprint import CircularFootprint
5
+ from simple_autonomous_car.footprint.rectangular_footprint import RectangularFootprint
6
+
7
+ __all__ = ["BaseFootprint", "RectangularFootprint", "CircularFootprint"]
@@ -0,0 +1,128 @@
1
+ """Base footprint class for vehicle shape representation."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ import numpy as np
6
+
7
+
8
+ class BaseFootprint(ABC):
9
+ """
10
+ Base class for vehicle footprints.
11
+
12
+ A footprint represents the shape of the vehicle in 2D space.
13
+ It's used to ensure the entire vehicle (not just a point) avoids obstacles.
14
+
15
+ Attributes
16
+ ----------
17
+ name : str
18
+ Footprint name/identifier.
19
+ """
20
+
21
+ def __init__(self, name: str = "footprint"):
22
+ """
23
+ Initialize base footprint.
24
+
25
+ Parameters
26
+ ----------
27
+ name : str, default="footprint"
28
+ Footprint name/identifier.
29
+ """
30
+ self.name = name
31
+
32
+ @abstractmethod
33
+ def get_vertices(self, position: np.ndarray, heading: float) -> np.ndarray:
34
+ """
35
+ Get footprint vertices in world frame.
36
+
37
+ Parameters
38
+ ----------
39
+ position : np.ndarray
40
+ Vehicle position [x, y] in world frame.
41
+ heading : float
42
+ Vehicle heading angle in radians.
43
+
44
+ Returns
45
+ -------
46
+ np.ndarray
47
+ Array of shape (N, 2) with vertex positions [[x1, y1], [x2, y2], ...].
48
+ """
49
+ pass
50
+
51
+ @abstractmethod
52
+ def get_bounding_radius(self) -> float:
53
+ """
54
+ Get bounding radius (distance from center to farthest point).
55
+
56
+ This is used for efficient collision checking and costmap inflation.
57
+
58
+ Returns
59
+ -------
60
+ float
61
+ Bounding radius in meters.
62
+ """
63
+ pass
64
+
65
+ @abstractmethod
66
+ def contains_point(self, point: np.ndarray, position: np.ndarray, heading: float) -> bool:
67
+ """
68
+ Check if a point is inside the footprint.
69
+
70
+ Parameters
71
+ ----------
72
+ point : np.ndarray
73
+ Point [x, y] in world frame.
74
+ position : np.ndarray
75
+ Vehicle position [x, y] in world frame.
76
+ heading : float
77
+ Vehicle heading angle in radians.
78
+
79
+ Returns
80
+ -------
81
+ bool
82
+ True if point is inside footprint, False otherwise.
83
+ """
84
+ pass
85
+
86
+ def get_inflation_radius(self, padding: float = 0.0) -> float:
87
+ """
88
+ Get inflation radius for costmap (bounding radius + padding).
89
+
90
+ Parameters
91
+ ----------
92
+ padding : float, default=0.0
93
+ Additional safety padding in meters.
94
+
95
+ Returns
96
+ -------
97
+ float
98
+ Inflation radius in meters.
99
+ """
100
+ return self.get_bounding_radius() + padding
101
+
102
+ def check_collision(self, obstacles: np.ndarray, position: np.ndarray, heading: float) -> bool:
103
+ """
104
+ Check if footprint collides with any obstacles.
105
+
106
+ Parameters
107
+ ----------
108
+ obstacles : np.ndarray
109
+ Array of obstacle positions [[x1, y1], [x2, y2], ...].
110
+ position : np.ndarray
111
+ Vehicle position [x, y] in world frame.
112
+ heading : float
113
+ Vehicle heading angle in radians.
114
+
115
+ Returns
116
+ -------
117
+ bool
118
+ True if collision detected, False otherwise.
119
+ """
120
+ if len(obstacles) == 0:
121
+ return False
122
+
123
+ # Simple collision check: if any obstacle is inside footprint
124
+ for obstacle in obstacles:
125
+ if self.contains_point(obstacle, position, heading):
126
+ return True
127
+
128
+ return False
@@ -0,0 +1,73 @@
1
+ """Circular footprint for vehicles."""
2
+
3
+ import numpy as np
4
+
5
+ from simple_autonomous_car.footprint.base_footprint import BaseFootprint
6
+
7
+
8
+ class CircularFootprint(BaseFootprint):
9
+ """
10
+ Circular footprint for vehicles.
11
+
12
+ Represents a vehicle as a circle (simpler but less accurate for cars).
13
+
14
+ Parameters
15
+ ----------
16
+ radius : float
17
+ Vehicle radius in meters.
18
+ name : str, default="circular_footprint"
19
+ Footprint name.
20
+ """
21
+
22
+ def __init__(self, radius: float, name: str = "circular_footprint"):
23
+ super().__init__(name=name)
24
+ self.radius = radius
25
+
26
+ def get_vertices(self, position: np.ndarray, heading: float) -> np.ndarray:
27
+ """
28
+ Get circle vertices (approximated as polygon) in world frame.
29
+
30
+ Parameters
31
+ ----------
32
+ position : np.ndarray
33
+ Vehicle position [x, y] in world frame.
34
+ heading : float
35
+ Vehicle heading angle (not used for circle, but kept for interface compatibility).
36
+
37
+ Returns
38
+ -------
39
+ np.ndarray
40
+ Array of shape (N, 2) with circle vertices (approximated as 16-sided polygon).
41
+ """
42
+ # Approximate circle as 16-sided polygon
43
+ num_vertices = 16
44
+ angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False)
45
+
46
+ vertices = position + self.radius * np.column_stack([np.cos(angles), np.sin(angles)])
47
+
48
+ return np.asarray(vertices, dtype=np.float64)
49
+
50
+ def get_bounding_radius(self) -> float:
51
+ """Get bounding radius (same as circle radius)."""
52
+ return self.radius
53
+
54
+ def contains_point(self, point: np.ndarray, position: np.ndarray, heading: float) -> bool:
55
+ """
56
+ Check if point is inside circle.
57
+
58
+ Parameters
59
+ ----------
60
+ point : np.ndarray
61
+ Point [x, y] in world frame.
62
+ position : np.ndarray
63
+ Vehicle position [x, y] in world frame.
64
+ heading : float
65
+ Vehicle heading angle (not used for circle).
66
+
67
+ Returns
68
+ -------
69
+ bool
70
+ True if point is inside circle.
71
+ """
72
+ distance = np.linalg.norm(point - position)
73
+ return bool(distance <= self.radius)
@@ -0,0 +1,123 @@
1
+ """Rectangular footprint for vehicles."""
2
+
3
+ import numpy as np
4
+
5
+ from simple_autonomous_car.footprint.base_footprint import BaseFootprint
6
+
7
+
8
+ class RectangularFootprint(BaseFootprint):
9
+ """
10
+ Rectangular footprint for vehicles.
11
+
12
+ Represents a vehicle as a rectangle (typical for cars).
13
+
14
+ Parameters
15
+ ----------
16
+ length : float
17
+ Vehicle length in meters (front to back).
18
+ width : float
19
+ Vehicle width in meters (left to right).
20
+ center_offset : np.ndarray, optional
21
+ Offset of footprint center from vehicle position [x, y] in vehicle frame.
22
+ Default: [0, 0] (center at vehicle position).
23
+ name : str, default="rectangular_footprint"
24
+ Footprint name.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ length: float,
30
+ width: float,
31
+ center_offset: np.ndarray | None = None,
32
+ name: str = "rectangular_footprint",
33
+ ):
34
+ super().__init__(name=name)
35
+ self.length = length
36
+ self.width = width
37
+ self.center_offset = center_offset if center_offset is not None else np.array([0.0, 0.0])
38
+
39
+ # Calculate bounding radius (half diagonal)
40
+ self._bounding_radius = np.sqrt((length / 2) ** 2 + (width / 2) ** 2)
41
+
42
+ def get_vertices(self, position: np.ndarray, heading: float) -> np.ndarray:
43
+ """
44
+ Get rectangle vertices in world frame.
45
+
46
+ Parameters
47
+ ----------
48
+ position : np.ndarray
49
+ Vehicle position [x, y] in world frame.
50
+ heading : float
51
+ Vehicle heading angle in radians.
52
+
53
+ Returns
54
+ -------
55
+ np.ndarray
56
+ Array of shape (4, 2) with rectangle corners.
57
+ """
58
+ # Define rectangle corners in vehicle frame (centered at origin)
59
+ half_length = self.length / 2
60
+ half_width = self.width / 2
61
+
62
+ corners_vehicle = np.array(
63
+ [
64
+ [half_length, half_width], # Front-right
65
+ [-half_length, half_width], # Rear-right
66
+ [-half_length, -half_width], # Rear-left
67
+ [half_length, -half_width], # Front-left
68
+ ]
69
+ )
70
+
71
+ # Apply center offset
72
+ corners_vehicle += self.center_offset
73
+
74
+ # Rotate by heading
75
+ cos_h = np.cos(heading)
76
+ sin_h = np.sin(heading)
77
+ rotation_matrix = np.array([[cos_h, -sin_h], [sin_h, cos_h]])
78
+ corners_rotated = (rotation_matrix @ corners_vehicle.T).T
79
+
80
+ # Translate to world position
81
+ vertices = corners_rotated + position
82
+
83
+ return np.asarray(vertices, dtype=np.float64)
84
+
85
+ def get_bounding_radius(self) -> float:
86
+ """Get bounding radius (half diagonal of rectangle)."""
87
+ return float(self._bounding_radius)
88
+
89
+ def contains_point(self, point: np.ndarray, position: np.ndarray, heading: float) -> bool:
90
+ """
91
+ Check if point is inside rectangle.
92
+
93
+ Parameters
94
+ ----------
95
+ point : np.ndarray
96
+ Point [x, y] in world frame.
97
+ position : np.ndarray
98
+ Vehicle position [x, y] in world frame.
99
+ heading : float
100
+ Vehicle heading angle in radians.
101
+
102
+ Returns
103
+ -------
104
+ bool
105
+ True if point is inside rectangle.
106
+ """
107
+ # Transform point to vehicle frame
108
+ point_relative = point - position
109
+
110
+ # Rotate by -heading to align with vehicle frame
111
+ cos_h = np.cos(-heading)
112
+ sin_h = np.sin(-heading)
113
+ rotation_matrix = np.array([[cos_h, -sin_h], [sin_h, cos_h]])
114
+ point_vehicle = rotation_matrix @ point_relative
115
+
116
+ # Apply center offset
117
+ point_vehicle -= self.center_offset
118
+
119
+ # Check if point is within rectangle bounds
120
+ half_length = self.length / 2
121
+ half_width = self.width / 2
122
+
123
+ return bool(abs(point_vehicle[0]) <= half_length and abs(point_vehicle[1]) <= half_width)
@@ -0,0 +1,21 @@
1
+ """Frame conversion utilities for coordinate transformations."""
2
+
3
+ from simple_autonomous_car.frames.frenet import (
4
+ FrenetFrame,
5
+ ego_to_frenet,
6
+ ego_to_sensor,
7
+ frenet_to_ego,
8
+ frenet_to_global,
9
+ global_to_frenet,
10
+ sensor_to_ego,
11
+ )
12
+
13
+ __all__ = [
14
+ "FrenetFrame",
15
+ "global_to_frenet",
16
+ "frenet_to_global",
17
+ "ego_to_frenet",
18
+ "frenet_to_ego",
19
+ "sensor_to_ego",
20
+ "ego_to_sensor",
21
+ ]