suboptimumg.log_analysis.yaw_fit#

suboptimumg.log_analysis.yaw_fit.apply_relaxation_lag(u_rad, V, t, L_f)[source]#

Lag the steering input by a first-order LPV filter, tau(V) = L_f / V.

Approximates front-tire relaxation by lagging the input rather than the full kinematic slip angle: exact at low frequency, slightly off at high frequency because the body terms (beta, lf*r/V) are not lagged. A cheap pre-filter that captures relaxation length’s dominant phase effect without a state-space rewrite. L_f <= 0 is a no-op.

Parameters:
  • u_rad (NDArray[np.float64]) – Steering input (rad).

  • V (NDArray[np.float64]) – Speed at each sample (m/s); clipped at V_FLOOR_MPS.

  • t (NDArray[np.float64]) – Uniformly-sampled time vector (s).

  • L_f (float) – Front relaxation length (m).

Returns:

Lagged input, same length as u_rad.

Return type:

NDArray[np.float64]

suboptimumg.log_analysis.yaw_fit.axle_slip_angles(w, irl_car)[source]#

Per-sample front and rear slip-angle magnitudes (rad).

|alpha_f| = |delta - (beta + lf r / V)| and |alpha_r| = |-beta + lr r / V|. Both derive purely from measured signals, so the optimizer sees them as a fixed time-series – there is no feedback through the simulated state during fitting.

Parameters:
  • w (WindowArrays) – Must carry both body_slip_rad and bicycle_steer_rad.

  • irl_car (IrlCar) – Source of the wheelbase and CG split.

Returns:

(|alpha_f|, |alpha_r|), each the length of w.t.

Return type:

tuple[NDArray[np.float64], NDArray[np.float64]]

Raises:

KeyError – If the body-slip or bicycle-steer channel is absent from w.

suboptimumg.log_analysis.yaw_fit.bicycle_coeffs(V, chassis, irl_car, Ca_f=None, Ca_r=None)[source]#

Linear-bicycle (K, T_z, omega_n, zeta) at each speed. Vectorised over V.

Parameters:
  • V (NDArray[np.float64]) – Speed(s) (m/s); must all be strictly positive.

  • chassis (ChassisParams) – The three chassis scalars. Izz is always taken from here.

  • irl_car (IrlCar) – Source of the mass, wheelbase, and CG split.

  • Ca_f (NDArray[np.float64], optional) – Per-sample cornering stiffnesses, overriding chassis. Used by the Ca-decay scheduler, where stiffness varies sample-by-sample.

  • Ca_r (NDArray[np.float64], optional) – Per-sample cornering stiffnesses, overriding chassis. Used by the Ca-decay scheduler, where stiffness varies sample-by-sample.

Returns:

Every field shaped like V.

Return type:

BicycleCoeffs

Notes

These are the coefficients of the standard 2-DOF linear bicycle transfer function from front tire steer (rad) to yaw rate (rad/s):

H(s; V) = K(V) wn(V)^2 (1 + T_z(V) s) / (s^2 + 2 zeta(V) wn(V) s + wn(V)^2)

All four follow analytically from the three chassis scalars plus the known geometry, which is why those three scalars are the only thing fit_yaw optimizes. A free-coefficient LPV polynomial was tried first and proved unidentifiable on near-limit lapping data: the optimizer slid along the K/T_z product ridge to nonsense (T_z ~ 2500 s, K -> 0) while still scoring 80-90% VAF. Tying every coefficient back to the bicycle, with hard prior bounds from the car YAML, removes that ridge.

Inside the prior box omega_n^2 is positive for any sensible understeer car; the clip below only guards the extreme-oversteer combinations the optimizer may briefly probe.

suboptimumg.log_analysis.yaw_fit.bicycle_points(chassis, irl_car, speeds)[source]#

Linear-region bicycle coefficients sampled at each of speeds.

Parameters:
Return type:

list[BicyclePoint]

suboptimumg.log_analysis.yaw_fit.build_fit_views(window_arrays, mode)[source]#

Build the (sub-window, sign, source id) views the residual is summed over.

Lapping data is rarely symmetric – a track run one direction steers mostly one way, which lets the optimizer trade a real steering offset against a fake one. Mirroring views balances the excitation around zero.

Parameters:
  • window_arrays (list of WindowArrays) – Sub-windows to build views from.

  • mode ({"none", "mirror_all", "flip_half"}) – "none" uses each sub-window once, unmirrored. "mirror_all" uses each one twice, at both signs. "flip_half" mirrors the half of the sub-windows with the most positive mean steer.

Return type:

list of FitView

Raises:

ValueError – If mode is unrecognised.

suboptimumg.log_analysis.yaw_fit.build_param_layout(spec, irl_car, bounds_pct=None, u_off_max_deg=10.0, alpha_init=1.0, alpha_bounds=(0.0, 50.0), p_init=1.0, p_bounds=(0.5, 4.0), L_f_init=0.3, L_f_bounds=(0.05, 1.5), pin=None)[source]#

Build the parameter vector for spec, boxed by the car’s priors.

Ca_f, Ca_r, Izz and u_off are always present. A Ca scheduler adds alpha_f, alpha_r, p; front relaxation adds L_f. The three chassis priors and their bounds come from the car YAML’s dynamics_setup.

Parameters:
  • spec (FitSpec) – Decides which optional parameters are appended.

  • irl_car (IrlCar) – Source of the dynamics_setup priors.

  • bounds_pct (dict[str, float], optional) – Overrides the YAML’s prior_bounds. Keys Ca_front_pct, Ca_rear_pct, Izz_pct.

  • u_off_max_deg (float, optional) – Symmetric bound on the steering-offset parameter (deg).

  • alpha_init (optional) – Initial guess and bounds for the per-axle Ca-decay rates.

  • alpha_bounds (optional) – Initial guess and bounds for the per-axle Ca-decay rates.

  • p_init (optional) – Initial guess and bounds for the shared Ca-decay exponent.

  • p_bounds (optional) – Initial guess and bounds for the shared Ca-decay exponent.

  • L_f_init (optional) – Initial guess and bounds for the front relaxation length (m).

  • L_f_bounds (optional) – Initial guess and bounds for the front relaxation length (m).

  • pin (dict[str, float], optional) – Parameters to hold fixed at the given values instead of fitting.

Return type:

ParamLayout

Raises:

ValueError – If irl_car carries no dynamics_setup priors, or pin names a parameter this spec does not have.

suboptimumg.log_analysis.yaw_fit.compare_fits(fits)[source]#

Table comparing several fits, keyed by display name.

Parameters:

fits (dict[str, YawFit])

Return type:

FitComparison

suboptimumg.log_analysis.yaw_fit.extract_window_arrays(sub, index, ctx)[source]#

Pull the aligned numpy arrays for one sub-window off a SingleRunData.

Every channel is joined onto the yaw-rate timestamp grid, so the returned arrays are equal-length regardless of residual per-channel grid differences. Angles are converted to radians per the unit fields on ctx.

Parameters:
  • sub (SingleRunData) – One sub-window, as produced by auto_segment / resample_subwindows.

  • index (int) – Position of sub in its parent list; becomes WindowArrays.sw_id.

  • ctx (FitContext) – Channel names and units.

Returns:

Aligned arrays for sub.

Return type:

WindowArrays

Raises:

KeyError – If a required channel is missing from sub.

suboptimumg.log_analysis.yaw_fit.fit_yaw(fit_views, window_arrays, spec, layout, ctx, balance_mode='none', verbose=0, max_nfev=350)[source]#

Fit spec to the sub-windows by bounded output-error least squares.

The optimizer minimises the concatenated residual across fit_views. Each view’s simulation is initialised at that view’s measured yaw rate, so the residual starts at zero and the optimizer never has to fight an initial-condition mismatch alongside the dynamics.

Parameters:
  • fit_views (list of FitView) – Views the residual is summed over; see build_fit_views.

  • window_arrays (list of WindowArrays) – Source sub-windows, re-simulated once at the fitted parameters to produce the reported predictions.

  • spec (FitSpec) – The variant to fit.

  • layout (ParamLayout) – The parameter vector and its bounds.

  • ctx (FitContext) – Channel/geometry metadata, recorded on the fit.

  • balance_mode (str, optional) – Not used here; only recorded on the fit so you can see later how the views were weighted. See build_fit_views.

  • verbose (optional) – Forwarded to scipy.optimize.least_squares.

  • max_nfev (optional) – Forwarded to scipy.optimize.least_squares.

Returns:

The fitted parameters and their per-sub-window diagnostics.

Return type:

YawFit

suboptimumg.log_analysis.yaw_fit.predict_yaw(w, x, spec, layout, irl_car, y_init=None, sign=1.0)[source]#

Predict one sub-window’s yaw rate under spec at parameter vector x.

The pipeline is: offset and mirror the input (u_eff = sign * (u - u_off)), optionally lag it for front-tire relaxation, optionally decay each axle’s cornering stiffness against its own slip angle, then run the LPV biquad.

Parameters:
  • w (WindowArrays) – The sub-window to predict over.

  • x (NDArray[np.float64]) – Free-parameter vector, aligned with layout.free.

  • spec (FitSpec) – Selects the Ca scheduler and relaxation flavor.

  • layout (ParamLayout) – Maps x (plus any pinned values) onto named parameters.

  • irl_car (IrlCar) – Source of the mass, wheelbase, and CG split.

  • y_init (float, optional) – Forced initial output; see simulate_lpv_yaw.

  • sign (float, optional) – Mirrors the input, for the sign-balanced fit views.

Returns:

Predicted yaw rate (rad/s), same length as w.t.

Return type:

NDArray[np.float64]

Notes

The two optional extensions sit on top of the linear bicycle core (see bicycle_coeffs). ca_scheduler="abs_alpha_per_axle" decays each axle’s cornering stiffness against its own measured slip-angle magnitude:

Ca_f(t) = Ca_f0 / (1 + alpha_f |alpha_f(t)|^p)

which is what lets one fit span the near-limit sub-windows where the tires have left the linear region. relaxation="front" lags the steering input by tau(V) = L_f / V, modelling front-tire relaxation length; see apply_relaxation_lag.

suboptimumg.log_analysis.yaw_fit.simulate_lpv_yaw(t, u_rad, V, chassis, irl_car, Ca_f=None, Ca_r=None, y_init=None)[source]#

Sample-by-sample Tustin biquad with V(t)-scheduled coefficients.

At each sample the four LPV coefficients are evaluated at that sample’s speed (and, when the Ca-decay scheduler is active, that sample’s cornering stiffnesses), then converted to a discrete biquad by Tustin substitution s = (2/dt)(z-1)/(z+1).

Parameters:
  • t (NDArray[np.float64]) – Uniformly-sampled time vector (s).

  • u_rad (NDArray[np.float64]) – Front tire steer input (rad).

  • V (NDArray[np.float64]) – Speed at each sample (m/s); clipped at V_FLOOR_MPS.

  • chassis (ChassisParams) – The three chassis scalars.

  • irl_car (IrlCar) – Source of the mass, wheelbase, and CG split.

  • Ca_f (NDArray[np.float64], optional) – Per-sample cornering stiffnesses; see bicycle_coeffs.

  • Ca_r (NDArray[np.float64], optional) – Per-sample cornering stiffnesses; see bicycle_coeffs.

  • y_init (float, optional) – Forced initial output (rad/s). The biquad states are set so y[0] equals it exactly. Passing the measured yaw rate at the sub-window start is the intended path during fitting: the residual at t=0 is then zero and the optimizer does not have to fight an initial-condition mismatch on top of the dynamics. Defaults to the model’s own steady state, K(V[0]) * u[0].

Returns:

Simulated yaw rate (rad/s), same length as t.

Return type:

NDArray[np.float64]

suboptimumg.log_analysis.yaw_fit.summarize_fit(fit, sample_speeds=(10.0, 15.0, 20.0))[source]#

Console summary of a fit plus its linear-region coefficients at sample_speeds.

The coefficients are the linear-region ones, i.e. evaluated with the Ca decay switched off, so they describe the car’s small-slip behaviour.

Parameters:
  • fit (YawFit)

  • sample_speeds (tuple[float, ...])

Return type:

str

suboptimumg.log_analysis.yaw_fit.validate_fit(val_window_arrays, fit)[source]#

Evaluate a frozen fit on held-out sub-windows – no re-optimisation.

AIC / BIC use the fit’s own parameter count, so they are directly comparable against the training numbers on fit.train.

Parameters:
  • val_window_arrays (list of WindowArrays) – Held-out sub-windows, typically from another session’s artifact.

  • fit (YawFit) – The frozen fit to evaluate.

Returns:

Predictions and metrics on val_window_arrays.

Return type:

Prediction

suboptimumg.log_analysis.yaw_fit.weighted_mean_u_deg(views)[source]#

Sample-weighted mean steering angle across views (deg).

Near zero means the views are sign-balanced; far from zero means the steering offset and the chassis parameters are fighting each other.

Parameters:

views (list[tuple[WindowArrays, float, int]])

Return type:

float