Solver Mathematics¶
This document explains how lap time is computed in the current quasi-steady solver
implementation (src/apexsim/simulation/profile.py).
1. Discretization and State¶
The track is represented in arc-length domain by points \(i = 0,\dots,N-1\) with:
- position \((s_i)\) [m]
- curvature \(\kappa_i\) [1/m]
- grade \(\gamma_i = dz/ds\) [-]
- banking angle \(\beta_i\) [rad]
Segment length: $$ \Delta s_i = s_{i+1} - s_i, \quad i=0,\dots,N-2. $$
The solver computes a speed profile \(v_i\) [m/s], then derives:
- longitudinal acceleration \(a_{x,i}\) [m/s²]
- lateral acceleration \(a_{y,i}\) [m/s²]
- lap time \(T\) [s]
2. Lateral Envelope (Cornering Limit)¶
A lateral speed limit \(v_{\text{lat},i}\) is computed at each point. Core relation: $$ |a_{y,i}| = v_i^2 |\kappa_i|. $$
Given lateral acceleration capacity \(a_{y,\text{lim},i}\): $$ v_{\text{lat},i} = \begin{cases} \sqrt{a_{y,\text{lim},i}/|\kappa_i|}, & |\kappa_i| > \varepsilon \ v_{\max}, & |\kappa_i| \le \varepsilon \end{cases} $$ with clipping to \([v_{\min}, v_{\max}]\).
2.1 Lateral Acceleration Capacity¶
SingleTrackModel.lateral_accel_limit(...) solves a fixed-point problem because tire force capacity depends
on normal load, and normal load depends on lateral acceleration through load transfer.
For fixed speed \(v\):
- Estimate axle loads from quasi-static vertical balance using current \(a_y\) guess.
- Compute front/rear lateral tire forces at a representative peak slip angle (Pacejka Magic Formula).
- Update lateral limit: $$ a_{y,\text{next}} = \max\left(a_{y,\min}, \frac{F_{y,f}+F_{y,r}}{m} + g\sin\beta\right). $$
- Repeat until \(|a_{y,\text{next}}-a_{y,\text{current}}|\le\text{tol}\) or max iterations.
3. Longitudinal Coupling via Friction Circle¶
Available longitudinal capability is reduced by lateral usage: $$ \lambda_i = \sqrt{\max\left(0, 1 - \left(\frac{|a_{y,\text{req},i}|}{a_{y,\text{lim},i}}\right)^2\right)}, \quad |a_{y,\text{req},i}| = v_i^2 |\kappa_i|. $$
- Drive limit: \(a_{x,\text{drive},i} = a_{x,\text{drive,max}}\,\lambda_i\)
- Brake limit: \(a_{x,\text{brake},i} = a_{x,\text{brake,max}}\,\lambda_i\)
This is a simplified isotropic friction-circle approximation.
4. Forward Pass (Acceleration-Limited)¶
Starting from \(v_0\), propagate forward with kinematic relation: $$ v_{i+1}^2 = v_i^2 + 2 a_{x,\text{net},i} \Delta s_i. $$
Initial-condition rule: $$ v_0 = \min\left(v_{\text{lat},0}, v_{\max}, v_{\text{init}}\right), $$ where \(v_{\text{init}}\) is:
RuntimeConfig.initial_speed, if provided.- otherwise the legacy fallback \(v_{\max}\).
Net acceleration model: $$ a_{x,\text{net},i} = a_{x,\text{drive},i} - \frac{D(v_i)}{m} - g\,\gamma_i, $$ where drag force is $$ D(v) = \tfrac{1}{2}\rho c_d A v^2. $$
Then enforce bounds: $$ v_{i+1} \leftarrow \min(v_{i+1}, v_{\text{lat},i+1}, v_{\max}), \quad v_{i+1} \ge v_{\min}. $$
5. Backward Pass (Braking-Limited)¶
From the end of the lap backwards, enforce braking feasibility: $$ v_i^2 = v_{i+1}^2 + 2 a_{x,\text{decel,avail},i} \Delta s_i, $$ with $$ a_{x,\text{decel,avail},i} = a_{x,\text{brake},i} + \frac{D(v_{i+1})}{m} + g\,\gamma_{i+1}. $$
Again clamp by lateral and global speed bounds.
6. Final Accelerations and Lap Time¶
After forward/backward constraints, the final profile is \(v_i\). Longitudinal acceleration is reconstructed by finite differences: $$ a_{x,i} = \frac{v_{i+1}^2 - v_i^2}{2\Delta s_i}, \quad i=0,\dots,N-2. $$
Lateral acceleration: $$ a_{y,i} = v_i^2\kappa_i. $$
Segment time is approximated with average segment speed: $$ \Delta t_i = \frac{\Delta s_i}{\max\left(\tfrac{v_i+v_{i+1}}{2}, \varepsilon_v\right)}. $$
Total lap time: $$ T = \sum_{i=0}^{N-2} \Delta t_i. $$
7. Numerical Convergence Controls¶
Lateral envelope convergence in solve_speed_profile(...) is configurable via
SimulationConfig.numerics:
lateral_envelope_max_iterationslateral_envelope_convergence_tolerance
Stop criterion: $$ \max_i |v^{(k)}{\text{lat},i} - v^{(k-1)}{\text{lat},i}| \le \text{tol}_v. $$
The actual number of iterations used is reported as
SpeedProfileResult.lateral_envelope_iterations.
8. Physical Scope and Limitations¶
Current solver is intentionally quasi-steady:
- No transient tire relaxation dynamics.
- No explicit driver/controller model in the speed-profile pass.
- Longitudinal limits are envelope-based constants (drive/brake maxima).
- Tire model uses fixed representative peak slip-angle for envelope estimation.
These simplifications keep the solver fast and stable, while preserving core constraints for lap-time studies.
The same solver routine can be used with different backends implementing
VehicleModel, including the single-track and point-mass models.
9. Equation-to-Code Mapping¶
- \(a_{y,i} = v_i^2\kappa_i\):
src/apexsim/simulation/profile.py(solve_speed_profile,ay = ...)- \(v_{\text{lat},i} = \sqrt{a_{y,\text{lim},i}/|\kappa_i|}\) (with clipping):
src/apexsim/simulation/envelope.py(lateral_speed_limit)src/apexsim/simulation/profile.py(solve_speed_profile,v_lat[idx] = ...)- Friction-circle scaling \(\lambda_i\) (vehicle-model dependent):
src/apexsim/vehicle/single_track_model.py(_friction_circle_scale)- Forward pass \(v_{i+1}^2 = v_i^2 + 2a\Delta s\):
src/apexsim/simulation/profile.py(solve_speed_profile,next_speed_sq = ...)- Backward pass braking feasibility:
src/apexsim/simulation/profile.py(solve_speed_profile,entry_speed_sq = ...)- Lap-time accumulation \(T = \sum \Delta t_i\):
src/apexsim/simulation/profile.py(lap_time += _segment_dt(...))- Segment time model \(\Delta t_i = \Delta s_i / \bar v_i\):
src/apexsim/simulation/profile.py(_segment_dt)- Lateral limit fixed-point update:
src/apexsim/vehicle/single_track_model.py(lateral_accel_limit)- Lateral envelope fixed-point convergence in speed domain:
src/apexsim/simulation/profile.py(for iteration_idx ...,max_delta_speed ...)- Vehicle-model API contract consumed by the solver:
src/apexsim/simulation/model_api.py(VehicleModel)src/apexsim/simulation/profile.py(solve_speed_profile, calls onmodel)
10. Differentiable Torch Solver API¶
For gradient-based workflows (e.g. autodiff sensitivities), ApexSim exposes:
apexsim.simulation.solve_speed_profile_torch(track, model, config)
This API returns tensor-valued outputs (TorchSpeedProfileResult) and keeps the
autograd graph intact for downstream backward() calls.
Current constraint:
RuntimeConfig.torch_compilemust beFalseforsolve_speed_profile_torch.