from enum import Enum
from typing import Literal, Optional
from pydantic import (
BaseModel,
ConfigDict,
Field,
computed_field,
field_validator,
model_validator,
)
from ..constants import in_to_m
[docs]
class DriveSetup(str, Enum):
REAR_WHEEL_DRIVE = "REAR_WHEEL_DRIVE"
FOUR_WHEEL_DRIVE = "FOUR_WHEEL_DRIVE"
[docs]
class SuspensionType(str, Enum):
SIMPLE = "Simple"
COMPLEX = "Complex"
[docs]
class AccumulatorModel(BaseModel):
model_config = ConfigDict(validate_assignment=True)
pack_weight: float = Field(gt=0, description="Battery pack weight (kg)")
[docs]
class DRSModel(BaseModel):
model_config = ConfigDict(validate_assignment=True)
drs_cd: float = Field(gt=0, description="DRS drag coefficient")
drs_cl: float = Field(gt=0, description="DRS lift coefficient")
drs_front_area: float = Field(gt=0, description="DRS frontal area (m²)")
drs_cop: float = Field(
gt=0, lt=1.0, description="DRS center of pressure (fraction toward front)"
)
drs_accel_thresh: float = Field(
ge=0, description="Acceleration threshold for DRS activation (m/s²)"
)
drs_present: bool = Field(description="Whether DRS is equipped")
[docs]
class AeroModel(BaseModel):
model_config = ConfigDict(validate_assignment=True)
cd: float = Field(ge=0, description="Drag coefficient (unitless)")
cl: float = Field(ge=0, description="Lift coefficient (unitless)")
front_area: float = Field(gt=0, description="Frontal area (m²)")
cop: float = Field(
gt=0, lt=1.0, description="Center of pressure (fraction toward front, 0-1)"
)
drs: Optional[DRSModel] = Field(None, description="DRS configuration")
[docs]
@field_validator("cd")
def validate_cd_plausible(cls, v):
if not (0.5 <= v <= 4):
print(
f"WARNING: Drag coefficient of {v} is outside recommended range [0.5, 4]"
)
return v
[docs]
@field_validator("cl")
def validate_cl_plausible(cls, v):
if not (0.25 <= v <= 4):
print(
f"WARNING: Lift coefficient of {v} is outside recommended range [0.25, 4]"
)
return v
[docs]
@field_validator("front_area")
def validate_front_area_plausible(cls, v):
if not (0.9 <= v <= 1.5):
print(
f"WARNING: Frontal area of {v} is outside recommended range [0.9, 1.5] m²"
)
return v
[docs]
@field_validator("cop")
def validate_cop_plausible(cls, v):
if not (0.2 <= v <= 0.8):
print(
f"WARNING: Center of pressure of {v} is outside recommended range [0.2, 0.8]"
)
return v
[docs]
class ChassisModel(BaseModel):
pass
[docs]
class DriverInterfaceModel(BaseModel):
model_config = ConfigDict(validate_assignment=True)
brake_bias: float = Field(
gt=0, lt=1.0, description="Front brake bias (0-1, where 1.0 = all front)"
)
seat_angle: float = Field(description="Driver seat angle (degrees)")
[docs]
@field_validator("brake_bias")
def validate_brake_bias_plausible(cls, v):
if v < 0.55:
print(
f"WARNING: Brake bias of {v} is too rear-biased for a typical racing application"
)
elif v > 0.8:
print(
f"WARNING: Brake bias of {v} is too front-biased for a typical racing application"
)
return v
[docs]
class MotorModel(BaseModel):
model_config = ConfigDict(validate_assignment=True)
name: str = Field(description="Product name")
max_rpm: int = Field(gt=0, description="Max RPM of motor")
fw_rpm: int = Field(gt=0, description="Field weakening RPM inflection point")
max_torque: int = Field(gt=0, description="Max torque of motor (Nm)")
fw_torque: int = Field(
gt=0, description="Field weakening torque inflection point (Nm)"
)
pow_lim: float = Field(gt=0, description="Power limit (W)")
efficiency_method: Literal["sys", "indiv"] = Field(
description="Efficiency calculation method: 'sys' or 'indiv'"
)
moc_efficiency: float = Field(
gt=0, le=1.0, description="Motor controller efficiency"
)
motor_efficiency: float = Field(gt=0, le=1.0, description="Motor efficiency")
chain_efficiency: float = Field(gt=0, le=1.0, description="Chain efficiency")
diff_efficiency: float = Field(
gt=0, le=1.0, description="Differential efficiency (currently unused)"
)
system_efficiency: float = Field(
gt=0,
le=1.0,
description="Alternative to product of individual efficiencies if efficiency_method is 'sys'",
)
mass: float = Field(gt=0, description="Motor mass (kg)")
ideal_gear_ratio: float = Field(
gt=0, description="Ideal gear ratio (currently unused)"
)
[docs]
@field_validator("fw_rpm")
def fw_rpm_less_than_max_rpm(cls, v, info):
if v > info.data["max_rpm"]:
raise ValueError("fw_rpm must be less than or equal to max_rpm")
return v
[docs]
@field_validator("fw_torque")
def fw_torque_less_than_max_torque(cls, v, info):
if v > info.data["max_torque"]:
raise ValueError("fw_torque must be less than or equal to max_torque")
return v
[docs]
@field_validator("pow_lim")
def validate_power_limit(cls, v):
if v < 25000:
raise ValueError(f"Power limit should not be less than 25000 W")
if v > 80000:
raise ValueError(f"Power limit should not be greater than 80000 W")
return v
[docs]
@field_validator("moc_efficiency")
def validate_moc_efficiency(cls, v):
if v < 0.95:
raise ValueError(f"MOC efficiency should not be less than 0.95")
return v
[docs]
@field_validator("motor_efficiency")
def validate_motor_efficiency(cls, v):
if v < 0.93:
raise ValueError(f"Motor efficiency should not be less than 0.93")
return v
[docs]
@field_validator("chain_efficiency")
def validate_chain_efficiency(cls, v):
if v < 0.9:
raise ValueError(f"Chain efficiency should not be less than 0.90")
return v
[docs]
@field_validator("max_rpm")
def validate_max_rpm_plausible(cls, v):
if not (1000 <= v <= 30000):
print(f"WARNING: Max RPM of {v} is outside recommended range [1000, 30000]")
return v
[docs]
@field_validator("fw_rpm")
def validate_fw_rpm_plausible(cls, v):
if not (1000 <= v <= 25000):
print(
f"WARNING: Field weakening RPM of {v} is outside recommended range [1000, 25000]"
)
return v
[docs]
@field_validator("max_torque")
def validate_max_torque_plausible(cls, v):
if not (20 <= v <= 500):
print(
f"WARNING: Max torque of {v} is outside recommended range [20, 500] Nm"
)
return v
[docs]
@field_validator("fw_torque")
def validate_fw_torque_plausible(cls, v):
if not (10 <= v <= 400):
print(
f"WARNING: Field weakening torque of {v} is outside recommended range [10, 400] Nm"
)
return v
[docs]
class PowertrainModel(BaseModel):
"""
Powertrain configuration including motor and transmission.
"""
model_config = ConfigDict(validate_assignment=True)
motor: MotorModel = Field(description="Motor configuration")
ratio: float = Field(gt=0, description="Gear ratio (wheel_rpm = motor_rpm / ratio)")
regen_percent: float = Field(
ge=0, le=1.0, description="Regenerative braking percentage (0-1)"
)
max_regen: int = Field(ge=0, description="Maximum regenerative power (W)")
coast_trigger: float = Field(description="Coasting trigger threshold")
coast_release_offset: float = Field(description="Coasting release offset")
[docs]
@field_validator("ratio")
def validate_ratio_plausible(cls, v):
if not (0.5 <= v <= 20):
print(f"WARNING: Gear ratio of {v} is outside recommended range [0.5, 20]")
return v
[docs]
@model_validator(mode="after")
def validate_regen_config(self):
if self.regen_percent == 0 and self.max_regen > 0:
print(
"WARNING: Max regen is non-zero but regen percent is zero. This will result in no regen braking"
)
if self.regen_percent == 1 and self.max_regen <= 0:
print(
"WARNING: Regen percent is 1 but max regen is not specified. This will result in no regen braking"
)
return self
[docs]
class SimpleSuspensionModel(BaseModel):
model_config = ConfigDict(validate_assignment=True)
type: Literal["Simple"] = Field(default="Simple", description="Suspension type")
[docs]
class ComplexSuspensionModel(BaseModel):
model_config = ConfigDict(validate_assignment=True)
type: Literal["Complex"] = Field(default="Complex", description="Suspension type")
front_roll_center_height: float = Field(
gt=0, description="Front roll center height (m)"
)
rear_roll_center_height: float = Field(
gt=0, description="Rear roll center height (m)"
)
front_roll_stiffness_k: float = Field(
gt=0, description="Front roll stiffness (Nm/rad)"
)
rear_roll_stiffness_k: float = Field(
gt=0, description="Rear roll stiffness (Nm/rad)"
)
[docs]
class TireModel(BaseModel):
model_config = ConfigDict(validate_assignment=True)
weight: float = Field(gt=0, description="Weight of each tire (N)")
tire_radius: float = Field(gt=0, description="Radius of each tire (inches)")
mu_lat_zero_load: float = Field(
gt=0, description="Lateral friction coefficient at zero load (unitless)"
)
mu_long_lat_ratio: float = Field(
gt=0,
description="Ratio of longitudinal to lateral friction (unitless)",
)
[docs]
@field_validator("weight")
def validate_weight_plausible(cls, v):
if v > 75:
print(
f"WARNING: Tire weight of {v} kg is outside recommended range (0, 75]"
)
return v
[docs]
@field_validator("tire_radius")
def validate_tire_radius_plausible(cls, v):
# Tire radius is in inches, convert tire diameter to meters for comparison
diameter_m = in_to_m(v * 2)
min_diam_m = in_to_m(8)
max_diam_m = in_to_m(24)
if not (min_diam_m <= diameter_m <= max_diam_m):
print(
f"WARNING: Tire diameter of {diameter_m:.4f} m is outside recommended range [{min_diam_m:.4f}, {max_diam_m:.4f}] m (8-24 inches)"
)
return v
[docs]
@field_validator("mu_lat_zero_load")
def validate_mu_lat_zero_load_plausible(cls, v):
if not (0.8 <= v <= 2.1):
print(
f"WARNING: Lateral friction coefficient of {v} is outside recommended range [0.8, 2.1]"
)
return v
[docs]
@field_validator("mu_long_lat_ratio")
def validate_mu_long_lat_ratio_plausible(cls, v):
if not (0.3 <= v <= 1.3):
print(
f"WARNING: Longitudinal/lateral friction ratio of {v} is outside recommended range [0.3, 1.3]"
)
return v
[docs]
class VehicleModel(BaseModel):
model_config = ConfigDict(validate_assignment=True)
# Vehicle mass and geometry
mass: float = Field(gt=0, description="Total vehicle mass (kg)")
w_distr_b: float = Field(
gt=0, lt=1, description="Rear weight distribution percentage (0-1)"
)
cg_h: float = Field(gt=0, description="Center of gravity height (m)")
wb: float = Field(gt=0, description="Wheelbase (m)")
front_track: float = Field(gt=0, description="Front track width (m)")
rear_track: float = Field(gt=0, description="Rear track width (m)")
rolling_coeff: float = Field(
ge=0, description="Rolling resistance coefficient (unitless)"
)
setup: DriveSetup = Field(description="Drive configuration")
@computed_field
@property
def w_distr_front(self) -> float:
return 1 - self.w_distr_b
@computed_field
@property
def track(self) -> float:
return (self.front_track + self.rear_track) / 2
# Subsystems
aero: AeroModel = Field(description="Aerodynamics configuration")
pwrtn: PowertrainModel = Field(description="Powertrain configuration")
accum: AccumulatorModel = Field(description="Accumulator configuration")
sus: SimpleSuspensionModel | ComplexSuspensionModel = Field(
discriminator="type", description="Suspension configuration"
)
tires: TireModel = Field(description="Tire parameters")
dri: DriverInterfaceModel = Field(description="Driver interface configuration")
chass: Optional[ChassisModel] = Field(None, description="Chassis configuration")
[docs]
@field_validator("rolling_coeff")
def validate_rolling_coeff(cls, v):
if v != 0.02:
raise ValueError(
f"Currently only rolling coefficient of 0.02 is considered valid"
)
return v