Spa Single-Track Sensitivity: Quasi-Static vs Transient¶
This notebook demonstrates how solver choice affects local sensitivity results. We run the same parameter study twice:
- Quasi-static speed-profile solver (torch backend, autodiff default)
- Transient PID solver (torch backend, autodiff sensitivities)
The goal is to highlight why yaw-inertia sensitivity is often near zero in quasi-static studies and becomes visible in transient analyses.
1. Engineering Question¶
For Spa-Francorchamps, how do four physical parameters affect:
- Lap time
- Energy consumption
Parameters:
- Vehicle mass
- CoG height
- Yaw inertia
- Drag coefficient
2. Local Sensitivity Method¶
For each objective $y$ and parameter $p_i$, we use local derivatives:
$$ S_i = \frac{\partial y}{\partial p_i} $$
For interpretation at +10% variation:
$$ \Delta y_{+10\%} \approx S_i \cdot (0.10 \cdot p_{i,0}) $$
We compare these deltas between quasi-static and transient solver paths.
3. Solver Assumptions and Expected Behavior¶
- Quasi-static solver neglects transient state dynamics, so parameters that primarily act through yaw transients (for example, yaw inertia) can appear weak.
- Transient PID solver evolves $v_x$, $v_y$, and yaw-rate states over the lap, so yaw-inertia effects can propagate into lap time and energy.
This is a model-path comparison at one operating point, not a global design-space claim.
from __future__ import annotations
from pathlib import Path
import sys
import pandas as pd
def find_repo_root(start: Path) -> Path:
for candidate in (start, *start.parents):
if (candidate / "pyproject.toml").exists() and (candidate / "src").exists():
return candidate
raise RuntimeError("Could not locate repository root from current working directory")
repo_root = find_repo_root(Path.cwd())
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
examples_sensitivity = repo_root / "examples" / "sensitivity"
if str(examples_sensitivity) not in sys.path:
sys.path.insert(0, str(examples_sensitivity))
from apexsim.analysis import SensitivityStudyParameter
from apexsim.simulation import (
TransientConfig,
TransientNumericsConfig,
TransientRuntimeConfig,
build_simulation_config,
)
from apexsim.track import load_track_csv
from common import (
build_solver_comparison_table,
example_vehicle_parameters,
plot_yaw_inertia_solver_comparison,
run_single_track_sensitivity_study,
sensitivity_output_root,
spa_track_path,
)
pd.set_option("display.max_columns", 60)
pd.set_option("display.width", 200)
from apexsim.vehicle import SingleTrackPhysics
variation_pct = 10.0
track = load_track_csv(spa_track_path())
parameter_definitions = [
SensitivityStudyParameter(name="mass", target="vehicle.mass", label="Vehicle mass"),
SensitivityStudyParameter(name="cg_height", target="vehicle.cg_height", label="Center of gravity height"),
SensitivityStudyParameter(name="yaw_inertia", target="vehicle.yaw_inertia", label="Yaw inertia"),
SensitivityStudyParameter(name="drag_coefficient", target="vehicle.drag_coefficient", label="Drag coefficient"),
]
root_output_dir = sensitivity_output_root() / "spa_single_track"
quasi_output_dir = root_output_dir / "quasi_static"
transient_output_dir = root_output_dir / "transient_pid_ad"
study_physics = SingleTrackPhysics(
max_steer_angle=0.3,
max_steer_rate=2.0,
reference_mass=example_vehicle_parameters().mass,
)
quasi_simulation_config = build_simulation_config(
compute_backend="torch",
torch_device="cpu",
torch_compile=False,
max_speed=115.0,
)
transient_simulation_config = build_simulation_config(
compute_backend="torch",
torch_device="cpu",
torch_compile=False,
max_speed=115.0,
initial_speed=12.0,
solver_mode="transient_oc",
transient=TransientConfig(
numerics=TransientNumericsConfig(max_time_step=1.0),
runtime=TransientRuntimeConfig(driver_model="pid", verbosity=0),
),
)
pd.DataFrame(
{
"parameter": [p.name for p in parameter_definitions],
"target": [p.target for p in parameter_definitions],
"variation_used": [f"+/-{variation_pct:.0f}%"] * len(parameter_definitions),
}
)
4. Quasi-Static Baseline (Torch + AD default)¶
quasi_long_df, quasi_pivot_df = run_single_track_sensitivity_study(
track=track,
track_label="Spa-Francorchamps",
output_dir=quasi_output_dir,
simulation_config=quasi_simulation_config,
parameters=parameter_definitions,
physics=study_physics,
)
quasi_long_df[[
"objective",
"parameter_label",
"sensitivity_raw",
"absolute_delta_plus",
"absolute_delta_minus",
]].sort_values(["objective", "parameter_label"], kind="stable")
5. Transient Study (PID + Autodiff)¶
transient_long_df, transient_pivot_df = run_single_track_sensitivity_study(
track=track,
track_label="Spa-Francorchamps (transient PID + autodiff)",
output_dir=transient_output_dir,
simulation_config=transient_simulation_config,
parameters=parameter_definitions,
physics=study_physics,
)
transient_long_df[[
"objective",
"parameter_label",
"sensitivity_raw",
"absolute_delta_plus",
"absolute_delta_minus",
]].sort_values(["objective", "parameter_label"], kind="stable")
6. Solver Comparison and Yaw-Inertia Focus¶
Now we merge both tables and compare the same local derivatives across solver paths.
comparison_df = build_solver_comparison_table(
quasi_static_long=quasi_long_df,
transient_long=transient_long_df,
)
comparison_df.to_csv(root_output_dir / "solver_comparison.csv", index=False)
plot_yaw_inertia_solver_comparison(
comparison_table=comparison_df,
path=root_output_dir / "solver_comparison_yaw_inertia.png",
)
comparison_df[comparison_df["parameter"] == "yaw_inertia"][[
"objective",
"sensitivity_raw_quasi_static",
"sensitivity_raw_transient_pid_ad",
"absolute_delta_plus_quasi_static",
"absolute_delta_plus_transient_pid_ad",
"sensitivity_raw_change",
"absolute_delta_plus_change",
]]
7. Engineering Interpretation¶
- If yaw-inertia sensitivity is near zero in the quasi-static run but non-zero in transient PID, the difference reflects solver-model assumptions, not a contradiction.
- Quasi-static remains useful for fast screening and trend studies.
- Transient analyses are preferred when control-rate limits and yaw-state dynamics materially influence the performance metric.
- For design decisions, use both views together: quasi-static for broad scanning, transient for dynamic-importance confirmation.