Simulating FSAE Vehicle Dynamics
Define every car as a parameter set, never as a branch in the simulation logic:
Python# vehicles.py CR30 = { "name": "Cr30", "season": "2025-2026", "mass_kg": 245.0, "wheelbase_m": 1.550, "track_front_m": 1.230, "track_rear_m": 1.200, "cg_height_m": 0.265, "weight_dist_front": 0.46, "tire_model": "pacejka_hoosier_r25b_18x6", "aero": {"cl": -2.8, "cd": 0.95, "cop_front_frac": 0.45, "ref_area_m2": 1.1}, "powertrain": {"engine": "cbr600", "redline_rpm": 14000, "final_drive": 3.8}, "suspension": {"roll_stiffness_f_nm_deg": 450, "roll_stiffness_r_nm_deg": 380}, } CR31 = { **CR30, # inherit CR30 as baseline, override only what's changed "name": "CR31", "season": "2026-2027", "mass_kg": 238.0, "aero": {**CR30["aero"], "cl": -3.2, "cop_front_frac": 0.47}, } sim_result_cr30 = run_lap_sim(CR30, track="endurance_2026") sim_result_cr31 = run_lap_sim(CR31, track="endurance_2026")
The simulation engine (run_lap_sim, tire model, aero map, etc.) takes a parameter dict as input and contains zero if car == "CR30" conditionals.
Progress:
- [ ] Extract all car-specific numbers into a parameter schema
- [ ] Build/confirm simulation engine reads only from the schema (no hardcoded constants)
- [ ] Define CR30 parameter set from current design data
- [ ] Define CR31 as CR30 + deltas (inheritance, not duplication)
- [ ] Validate CR30 sim against known test/endurance data (correlation check)
- [ ] Run CR31 sim, diff results against CR30 baseline
- [ ] Document every parameter source (CAD, test data, estimate, carryover)
- [ ] Version-control parameter files alongside sim code
-
Schema first. Before touching simulation code, write down every physical/geometric/electronic parameter that could plausibly differ between model years: mass, CG, geometry, tire model, aero coefficients, powertrain, suspension rates, driver inputs. This schema is the contract between "car design" and "simulation code."
-
One engine, many inputs. The lap sim, skidpad sim, acceleration sim, etc. must be pure functions of
(vehicle_params, track_params, driver_params). If you find yourself writingif vehicle["name"] == "CR31", that's a signal a parameter is missing from the schema — add it instead of branching. -
Inherit, don't duplicate. CR31 is defined as CR30 plus explicit overrides. This makes design deltas visible by diffing the dict, and prevents silently stale parameters when CR30 data is corrected.
-
Correlate before trusting. Validate the engine against CR30 using real test day / endurance data (if available) before relying on CR31 predictions, since CR31 has no physical twin yet.
-
Track provenance. Tag each parameter with its source (measured, CAD estimate, carried over, assumed) so reviewers know confidence level — critical for CR31 since much of it is still being built.
-
Diff, don't just report. When presenting CR31 results, always show delta vs CR30 (lap time, corner speeds, g-g diagram shift) so design changes are traceable to performance impact.
Example 1: Input: "Suspension team dropped front roll stiffness from 450 to 400 Nm/deg on CR31 only." Output:
PythonCR31["suspension"]["roll_stiffness_f_nm_deg"] = 400 # was 450 (CR30), test change per Suspension lead 2026-03-01
Re-run run_lap_sim(CR31, track); report lateral g and balance shift vs previous CR31 run and vs CR30, not just an absolute number.
Example 2:
Input: "We need skidpad time for both cars on the new tire compound."
Output: Add "tire_model": "pacejka_hoosier_r25b_18x6_newcompound" as an override in both CR30_newcompound and CR31_newcompound derived dicts (don't mutate baseline dicts — keep baseline reproducible), run the same run_skidpad_sim() for each, output a table: Car | Tire | Skidpad Time | Δ vs baseline compound.
- Keep parameter files in version control (JSON/YAML/Python dataclasses), reviewed like code.
- Use dataclasses or typed dicts with units in field names (
mass_kg, notmass) to prevent unit-mismatch bugs across model years. - Write regression tests: feed CR30 params through the engine and assert outputs match last known-good values whenever engine code changes.
- When CR31 lacks real data for a parameter, default to CR30's value and flag it explicitly (e.g.,
"cg_height_m": CR30["cg_height_m"], # TODO: CR31 CAD not finalized). - Separate "track/environment" params (surface mu, elevation, temperature) from "vehicle" params so the same car can be run across multiple events.
- Hardcoding car identity in logic (
if name == "Cr30") instead of adding a missing parameter — this guarantees the sim breaks or silently misbehaves for future cars (CR32, CR33...). - Copy-pasting CR30's full parameter block for CR31 and hand-editing — causes silent drift when CR30 data gets corrected later. Use inheritance/override instead.
- Mixing units (mm vs m, deg vs rad) between the two cars' data sources — enforce one unit system in the schema.
- Trusting CR31 outputs at face value — it has no on-track data yet; always present results as "relative to validated CR30 baseline," not absolute truth.
- Letting the simulation engine know about model years at all — the engine should be car-agnostic; only the parameter files should know "CR30" or "CR31" exists.