Source code for suboptimumg.vehicle.models

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