diff --git a/acc_project/controllers.py b/acc_project/controllers.py index 7500f7f..4794cfc 100644 --- a/acc_project/controllers.py +++ b/acc_project/controllers.py @@ -13,7 +13,10 @@ class PID: def update(self, error: float, dt: float) -> float: self._integral += error * dt - self._integral = max(self.integral_min, min(self.integral_max, self._integral)) + # Clamp the integral term to reduce windup when the error persists. + self._integral = max( + self.integral_min, min(self.integral_max, self._integral) + ) derivative = (error - self._previous_error) / dt if dt > 0.0 else 0.0 self._previous_error = error return self.kp * error + self.ki * self._integral + self.kd * derivative @@ -37,19 +40,24 @@ def compute_accel( lead_speed: float, dt: float, ) -> tuple[float, dict]: + # Speed loop: track the desired cruise speed when the road ahead is free. speed_error = self.cruise_speed - ego_speed desired_gap = self.desired_gap(ego_speed) + # Gap loop: positive gap_error means the actual distance is larger than required. gap_error = gap - desired_gap relative_speed = lead_speed - ego_speed cruise_accel = self.speed_pid.update(speed_error, dt) + # Relative speed is blended into the spacing loop to make the response less myopic. gap_accel = self.gap_pid.update(gap_error + 0.6 * relative_speed, dt) if gap < desired_gap + 5.0: + # Near the lead car, prioritize spacing so safety dominates comfort/speed tracking. commanded_accel = min(cruise_accel, gap_accel) mode = "gap-control" else: commanded_accel = cruise_accel + # Far from the lead car, behave like a standard cruise controller. mode = "speed-control" telemetry = { diff --git a/acc_project/models.py b/acc_project/models.py index 0f7663c..f43be1c 100644 --- a/acc_project/models.py +++ b/acc_project/models.py @@ -30,11 +30,19 @@ class EgoVehicleModel: dt: float max_accel: float = 2.5 max_decel: float = -4.0 + # This is a simple linear drag surrogate for learning purposes. drag_coeff: float = 0.03 def step(self, state: VehicleState, commanded_accel: float) -> VehicleState: - accel = max(self.max_decel, min(self.max_accel, commanded_accel)) - net_accel = accel - self.drag_coeff * state.speed + # Actuator/vehicle limits: the controller may ask for more than the car can produce. + accel = max( + self.max_decel, min(self.max_accel, commanded_accel) + ) + # A more detailed longitudinal model often includes rolling resistance and v^2 aero drag. + # Here we keep a linear term so the first project stays easy to read and tune. + net_accel = ( + accel - self.drag_coeff * state.speed + ) next_speed = max(0.0, state.speed + net_accel * self.dt) next_position = state.position + next_speed * self.dt return VehicleState( diff --git a/acc_project/simulation.py b/acc_project/simulation.py index f8dd182..889ea68 100644 --- a/acc_project/simulation.py +++ b/acc_project/simulation.py @@ -8,6 +8,7 @@ @dataclass class SimulationConfig: + # dataclass is a lightweight way to bundle simulation parameters together. total_time: float = 30.0 dt: float = 0.1 initial_gap: float = 35.0 @@ -15,6 +16,7 @@ class SimulationConfig: def run_simulation(config: SimulationConfig) -> list[dict]: + # list[dict] is a type hint: this function returns a list of telemetry records. scenario = LeadVehicleScenario(initial_position=config.initial_gap) ego_model = EgoVehicleModel(dt=config.dt) controller = ACCController() @@ -33,11 +35,14 @@ def run_simulation(config: SimulationConfig) -> list[dict]: lead.position += lead.speed * config.dt gap = lead.position - ego.position - command_accel, telemetry = controller.compute_accel( - ego_speed=ego.speed, - gap=gap, - lead_speed=lead.speed, - dt=config.dt, + command_accel, telemetry = ( + # The controller uses spacing and relative-speed errors to compute a desired accel. + controller.compute_accel( + ego_speed=ego.speed, + gap=gap, + lead_speed=lead.speed, + dt=config.dt, + ) ) ego = ego_model.step(ego, command_accel) @@ -65,7 +70,9 @@ def summarize(history: list[dict]) -> dict: min_gap = min(row["gap_m"] for row in history) max_speed = max(row["ego_speed_mps"] for row in history) avg_gap_error = sum(abs(row["gap_error_m"]) for row in history) / len(history) - gap_control_ratio = sum(1 for row in history if row["mode"] == "gap-control") / len(history) + gap_control_ratio = sum(1 for row in history if row["mode"] == "gap-control") / len( + history + ) return { "min_gap_m": min_gap, "max_ego_speed_mps": max_speed, diff --git a/main.py b/main.py index 347bd85..f3171ff 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,12 @@ from pathlib import Path -from acc_project.simulation import SimulationConfig, plot_history, run_simulation, summarize, write_csv +from acc_project.simulation import ( + SimulationConfig, + plot_history, + run_simulation, + summarize, + write_csv, +) def main() -> None: