"""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)"
)