import numpy as np
import numpy.typing as npt
from ..constants import G
from .vehicle import Car
from .vehicle_models import ExtendedVehicleModel
[docs]
class IrlCar(Car):
"""Car extended with real-world session setup parameters for log analysis.
Accepts an :class:`ExtendedVehicleModel` which bundles both the simulation
car parameters (mass, geometry, aero, etc.) and the session-specific setup
(alignment, ackermann curve, suspension stiffness/motion ratios).
Parameters
----------
vehicle_model : ExtendedVehicleModel
Unified model containing both car.yaml and vehicle_setup.yaml data.
"""
def __init__(self, vehicle_model: ExtendedVehicleModel):
super().__init__(vehicle_model)
self.align = vehicle_model.alignment
self.ackermann = vehicle_model.steering_setup.ackermann
self.sus_setup = vehicle_model.suspension_setup
[docs]
def right_tire_angle(self, sw_deg: float | np.ndarray) -> np.ndarray:
"""Right tire steer angle from the ackermann curve.
Positive sw_deg = right turn (right tire is inner, steers more).
"""
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)
[docs]
def roll_stiffness_total(self) -> float:
"""Total roll stiffness (front + rear) in Nm/rad."""
return (
self.sus_setup.roll_stiffness_front_Nm_per_rad
+ self.sus_setup.roll_stiffness_rear_Nm_per_rad
)
[docs]
def roll_stiffness_distribution(self) -> float:
"""Front roll stiffness as a fraction of total (0-1)."""
return (
self.sus_setup.roll_stiffness_front_Nm_per_rad / self.roll_stiffness_total()
)
[docs]
def lateral_weight_transfer(
self, ay_g: float | np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
"""Lateral weight transfer per axle (elastic component only).
Parameters
----------
ay_g : float or array
Lateral acceleration in g's (positive = turning left).
Returns
-------
(front_wt_N, rear_wt_N)
Elastic weight transfer at front and rear axles (N).
Positive = load transfer to the outside wheel.
"""
ay_g = np.asarray(ay_g, dtype=np.float64)
ay = ay_g * G
total_lateral_force = self.params.mass * ay
roll_dist = self.roll_stiffness_distribution()
roll_moment = total_lateral_force * self.params.cg_h
front_wt = roll_dist * roll_moment / self.params.front_track
rear_wt = (1.0 - roll_dist) * roll_moment / self.params.rear_track
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 = forward accel).
Returns
-------
float or np.ndarray
Change in front axle normal force (N). Negative under
acceleration, positive under braking.
"""
ax_g = np.asarray(ax_g, dtype=np.float64)
wt_total = self.params.mass * ax_g * G * self.params.cg_h / self.params.wb
return -wt_total
[docs]
def pot_to_heave(
self, pot_mm: float | npt.NDArray[np.float64], axle: str
) -> float | npt.NDArray[np.float64]:
"""Converts shock pot displacement (mm) to chassis heave displacement (m).
Parameters
----------
pot_mm : float | npt.NDArray[np.float64]
Shock pot displacement in millimeters. Positive = compression.
axle : str
"front" or "rear", used to select the correct motion ratio.
Returns
-------
float | npt.NDArray[np.float64]
Heave displacement in meters. Positive = compression.
"""
if axle == "front":
mr = self.sus_setup.motion_ratio_front
elif axle == "rear":
mr = self.sus_setup.motion_ratio_rear
else:
raise ValueError(f"axle must be 'front' or 'rear', got '{axle}'")
return np.asarray(pot_mm, dtype=np.float64) / 1000.0 * mr
[docs]
def heave_to_force(
self, heave_m: float | npt.NDArray[np.float64], axle: str
) -> float | npt.NDArray[np.float64]:
"""Converts Heave displacement (m) to vertical spring force (N).
Parameters
----------
heave_m : float | npt.NDArray[np.float64]
Heave displacement in meters. Positive = compression.
axle : str
"front" or "rear", used to select the correct heave stiffness.
Returns
-------
float | npt.NDArray[np.float64]
Vertical spring force in Newtons. Positive = compression.
"""
if axle == "front":
k = self.sus_setup.heave_stiffness_front_N_per_m
elif axle == "rear":
k = self.sus_setup.heave_stiffness_rear_N_per_m
else:
raise ValueError(f"axle must be 'front' or 'rear', got '{axle}'")
return k * np.asarray(heave_m, dtype=np.float64)
[docs]
def pot_to_force(
self, pot_mm: float | npt.NDArray[np.float64], axle: str
) -> float | npt.NDArray[np.float64]:
"""Converts shock pot displacement (mm) to vertical spring force (N).
Parameters
----------
pot_mm : float | npt.NDArray[np.float64]
Shock pot displacement in millimeters. Positive = compression.
axle : str
"front" or "rear", used to select the correct motion ratio and
heave stiffness.
Returns
-------
float | npt.NDArray[np.float64]
Vertical spring force in Newtons. Positive = compression.
"""
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 | npt.NDArray[np.float64],
rear_pot_delta_mm: float | npt.NDArray[np.float64],
ax_g: float | npt.NDArray[np.float64] = 0.0,
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""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 zero-aero
reference state (mm). Positive = compression.
ax_g
Longitudinal acceleration in g's. Used to subtract the
spring-borne portion of longitudinal weight transfer
(accounting for anti-dive/squat).
Returns
-------
(front_aero_N, rear_aero_N)
Estimated aero downforce at each axle (N).
"""
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")
wt_total = self.params.mass * ax_g * G * self.params.cg_h / self.params.wb
wt_front_spring = -wt_total * (1.0 - self.sus_setup.anti_dive_pct)
wt_rear_spring = wt_total * (1.0 - self.sus_setup.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
)