Source code for suboptimumg.loganalysis.irl_car

"""IrlCar — real-world car wrapper combining Car + vehicle_setup parameters."""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from suboptimumg.constants import G

if TYPE_CHECKING:
    from suboptimumg.vehicle.vehicle import Car

    from .fitted_curve import FittedCurve


[docs] class IrlCar: """Car + real-world setup parameters for log analysis. Wraps a :class:`Car` instance (loaded from ``car.yaml``) with the extra session-specific parameters from ``vehicle_setup.yaml``. Provides derived computation methods that combine both sources. Parameters ---------- car : Car Simulation car object (carries mass, wheelbase, track widths, CG height, etc. via ``car.params``). setup : dict Processed vehicle setup dict (from :func:`load_vehicle_setup`). Curve entries should already be :class:`FittedCurve` objects. """ def __init__(self, car: Car, setup: dict): print( """ DEPRECATION WARNING: suboptimumg.loganalysis.IrlCar is deprecated and will be removed in a future release. Please use suboptimumg.vehicle.IrlCar instead. """ ) self.car = car self.setup = setup # --- From Car.params (VehicleModel) --- p = car.params self.mass: float = p.mass self.wheelbase: float = p.wb self.front_track: float = p.front_track self.rear_track: float = p.rear_track self.cg_height: float = p.cg_h self.w_distr_front: float = p.w_distr_front # --- From setup dict — alignment --- align = setup["alignment"] self.camber_front_deg: float = align["camber_front_deg"] self.camber_rear_deg: float = align["camber_rear_deg"] self.toe_front_deg: float = align["toe_front_deg"] self.toe_rear_deg: float = align["toe_rear_deg"] # --- Steering --- self.ackermann: FittedCurve = setup["steering"]["ackermann"] # --- Suspension --- sus = setup["suspension"] self.roll_stiffness_front: float = sus["roll_stiffness_front_Nm_per_rad"] self.roll_stiffness_rear: float = sus["roll_stiffness_rear_Nm_per_rad"] self.heave_stiffness_front: float = sus["heave_stiffness_front_N_per_m"] self.heave_stiffness_rear: float = sus["heave_stiffness_rear_N_per_m"] self.anti_dive_pct: float = sus["anti_dive_pct"] self.anti_squat_pct: float = sus["anti_squat_pct"] self.motion_ratio_front: float = sus["motion_ratio_front"] self.motion_ratio_rear: float = sus["motion_ratio_rear"] # --- From Car.params (VehicleModel) — aero & rolling --- self.front_area: float = p.aero.front_area self.rolling_coeff: float = p.rolling_coeff # ----------------------------------------------------------------- # Construction helpers # -----------------------------------------------------------------
[docs] @classmethod def from_yaml( cls, car_yaml: str = "parameters/car.yaml", setup_yaml: str = "parameters/vehicle_setup.yaml", ) -> IrlCar: """Load both YAML files and construct an IrlCar.""" print( """ DEPRECATION WARNING: suboptimumg.loganalysis.IrlCar is deprecated and will be removed in a future release. Please use suboptimumg.vehicle.IrlCar instead. """ ) from suboptimumg.yaml import load_car_from_yaml from .variables import load_vehicle_setup car = load_car_from_yaml(car_yaml) setup = load_vehicle_setup(setup_yaml) return cls(car, setup)
# ----------------------------------------------------------------- # Steering # -----------------------------------------------------------------
[docs] def right_tire_angle(self, sw_deg: float | np.ndarray) -> np.ndarray: """Right tire steer angle from the ackermann curve. Parameters ---------- sw_deg : float or array Steering wheel angle in degrees. Positive = right turn (right tire is inner, steers more); negative = left turn (right tire is outer, steers less). """ return self.ackermann(sw_deg)
[docs] def left_tire_angle(self, sw_deg: float | np.ndarray) -> np.ndarray: """Left tire steer angle via ackermann symmetry: ``-f(-x)``.""" return -self.ackermann(-np.asarray(sw_deg, dtype=np.float64))
[docs] def tire_angles(self, sw_deg: float | np.ndarray) -> tuple[np.ndarray, np.ndarray]: """Return ``(left_deg, right_deg)`` for a given steering input.""" return self.left_tire_angle(sw_deg), self.right_tire_angle(sw_deg)
# ----------------------------------------------------------------- # Roll stiffness # -----------------------------------------------------------------
[docs] def roll_stiffness_total(self) -> float: """Total roll stiffness (front + rear) in Nm/rad.""" return self.roll_stiffness_front + self.roll_stiffness_rear
[docs] def roll_stiffness_distribution(self) -> float: """Front roll stiffness as a fraction of total (0-1).""" return self.roll_stiffness_front / self.roll_stiffness_total()
# ----------------------------------------------------------------- # Weight transfer # -----------------------------------------------------------------
[docs] def lateral_weight_transfer( self, ay_g: float | np.ndarray ) -> tuple[np.ndarray, np.ndarray]: """Lateral weight transfer per axle. Combines elastic (roll stiffness distribution) and geometric (direct load path through CG) components. Parameters ---------- ay_g : float or array Lateral acceleration in g's (positive = turning left). Returns ------- (front_wt_N, rear_wt_N) Weight transfer at the front and rear axles in Newtons. Positive values indicate load transfer to the outside wheel. """ ay_g = np.asarray(ay_g, dtype=np.float64) ay = ay_g * G total_lateral_force = self.mass * ay # Geometric (non-roll) component — splits by static weight dist # This is the direct lateral force component through the CG wt_geo_front = ( self.w_distr_front * total_lateral_force * self.cg_height / self.front_track ) wt_geo_rear = ( (1.0 - self.w_distr_front) * total_lateral_force * self.cg_height / self.rear_track ) # Elastic (roll couple) component — splits by roll stiffness roll_dist = self.roll_stiffness_distribution() roll_moment = total_lateral_force * self.cg_height wt_elastic_front = roll_dist * roll_moment / self.front_track wt_elastic_rear = (1.0 - roll_dist) * roll_moment / self.rear_track # Total is the sum of both (geometric already included in the # roll moment derivation for a simplified model, but we keep # them separate for clarity — the standard LLTD approach uses # roll stiffness distribution on the elastic portion only) front_wt = wt_elastic_front rear_wt = wt_elastic_rear return front_wt, rear_wt
[docs] def longitudinal_weight_transfer( self, ax_g: float | np.ndarray ) -> float | np.ndarray: """Longitudinal weight transfer on the front axle. Parameters ---------- ax_g : float or array Longitudinal acceleration in g's (positive = accelerating forward, negative = braking). Returns ------- float or np.ndarray Change in front axle normal force (N). Negative under acceleration (rear loads up), positive under braking (front loads up). Anti-dive/squat percentages reduce the geometric weight transfer through the springs. """ ax_g = np.asarray(ax_g, dtype=np.float64) ax = ax_g * G wt_total = self.mass * ax * self.cg_height / self.wheelbase # Anti-dive (braking) reduces front spring compression # Anti-squat (accel) reduces rear spring compression # Both reduce the *sprung mass* pitch but the total WT is the same. # Here we return the total geometric WT (anti features affect # ride height, not net normal force). return -wt_total
# ----------------------------------------------------------------- # Suspension math # ----------------------------------------------------------------- def _mr(self, axle: str) -> float: """Return motion ratio for 'front' or 'rear'.""" if axle == "front": return self.motion_ratio_front if axle == "rear": return self.motion_ratio_rear raise ValueError(f"axle must be 'front' or 'rear', got '{axle}'") def _heave_k(self, axle: str) -> float: """Return heave stiffness (N/m) for 'front' or 'rear'.""" if axle == "front": return self.heave_stiffness_front if axle == "rear": return self.heave_stiffness_rear raise ValueError(f"axle must be 'front' or 'rear', got '{axle}'")
[docs] def pot_to_heave(self, pot_mm: float | np.ndarray, axle: str) -> float | np.ndarray: """Shock pot displacement (mm) -> chassis heave displacement (m). ``heave_m = pot_mm / 1000 * MR`` """ return np.asarray(pot_mm, dtype=np.float64) / 1000.0 * self._mr(axle)
[docs] def heave_to_force( self, heave_m: float | np.ndarray, axle: str ) -> float | np.ndarray: """Heave displacement (m) -> vertical spring force (N). ``F = heave_stiffness * heave_m`` """ return self._heave_k(axle) * np.asarray(heave_m, dtype=np.float64)
[docs] def pot_to_force(self, pot_mm: float | np.ndarray, axle: str) -> float | np.ndarray: """Shock pot displacement (mm) -> vertical spring force (N). Combines :meth:`pot_to_heave` and :meth:`heave_to_force`. """ return self.heave_to_force(self.pot_to_heave(pot_mm, axle), axle)
[docs] def aero_force_from_pots( self, front_pot_delta_mm: float | np.ndarray, rear_pot_delta_mm: float | np.ndarray, ax_g: float | np.ndarray = 0.0, ) -> tuple[np.ndarray, np.ndarray]: """Estimate front/rear aero downforce from shock pot deltas. Parameters ---------- front_pot_delta_mm, rear_pot_delta_mm Change in average axle pot displacement from a reference (zero-aero) state, in mm. Positive = compression. ax_g Longitudinal acceleration in g's (positive = forward accel, negative = braking). Used to subtract weight transfer through the springs (accounting for anti-dive/squat). Returns ------- (front_aero_N, rear_aero_N) Estimated aero downforce at each axle in Newtons. """ ax_g = np.asarray(ax_g, dtype=np.float64) f_front_spring = self.pot_to_force(front_pot_delta_mm, "front") f_rear_spring = self.pot_to_force(rear_pot_delta_mm, "rear") # Longitudinal weight transfer (total, geometric) wt_total = self.mass * ax_g * G * self.cg_height / self.wheelbase # Under braking (ax_g < 0): wt_total < 0 → front loads up. # Anti-dive reduces the fraction that goes through front springs. # Under accel (ax_g > 0): wt_total > 0 → rear loads up. # Anti-squat reduces the fraction through rear springs. wt_front_spring = -wt_total * (1.0 - self.anti_dive_pct) wt_rear_spring = wt_total * (1.0 - self.anti_squat_pct) front_aero = f_front_spring - wt_front_spring rear_aero = f_rear_spring - wt_rear_spring return np.asarray(front_aero, dtype=np.float64), np.asarray( rear_aero, dtype=np.float64 )
# ----------------------------------------------------------------- # Repr # ----------------------------------------------------------------- def __repr__(self): return ( f"IrlCar(mass={self.mass:.1f} kg, " f"wb={self.wheelbase:.3f} m, " f"camber=({self.camber_front_deg}/{self.camber_rear_deg}) deg, " f"toe=({self.toe_front_deg}/{self.toe_rear_deg}) deg)" )