suboptimumg.loganalysis#

Log analysis utilities for PERDA-based CAN data.

class suboptimumg.loganalysis.FittedCurve(data, poly_order)[source]#

Bases: object

Polynomial fit of tabulated [x, y] data with Horner evaluation.

Parameters:
  • data (list of [x, y] pairs) – Raw tabulated input data.

  • poly_order (int) – Polynomial degree for the least-squares fit.

x_raw, y_raw

Original input arrays.

Type:

np.ndarray

poly_order#
Type:

int

coeffs#

Polynomial coefficients in descending degree order [c_n, c_{n-1}, ..., c_1, c_0], matching np.polyfit output and the order Horner’s method consumes directly.

Type:

np.ndarray

evaluate(x)[source]#

Evaluate the fitted polynomial via Horner’s method.

Parameters:

x (float or array-like) – Input value(s) at which to evaluate.

Returns:

Evaluated polynomial value(s).

Return type:

np.ndarray or np.floating

class suboptimumg.loganalysis.IrlCar(car, setup)[source]#

Bases: object

Car + real-world setup parameters for log analysis.

Wraps a Car instance (loaded from car.yaml) with the extra session-specific parameters from vehicle_setup.yaml. Provides derived computation methods that combine both sources.

Parameters:
  • car (Car) – Simulation car object (carries mass, wheelbase, track widths, CG height, etc. via car.params).

  • setup (dict) – Processed vehicle setup dict (from load_vehicle_setup()). Curve entries should already be FittedCurve objects.

aero_force_from_pots(front_pot_delta_mm, rear_pot_delta_mm, ax_g=0.0)[source]#

Estimate front/rear aero downforce from shock pot deltas.

Parameters:
  • front_pot_delta_mm (float | ndarray) – Change in average axle pot displacement from a reference (zero-aero) state, in mm. Positive = compression.

  • rear_pot_delta_mm (float | ndarray) – Change in average axle pot displacement from a reference (zero-aero) state, in mm. Positive = compression.

  • ax_g (float | ndarray) – Longitudinal acceleration in g’s (positive = forward accel, negative = braking). Used to subtract weight transfer through the springs (accounting for anti-dive/squat).

Returns:

Estimated aero downforce at each axle in Newtons.

Return type:

(front_aero_N, rear_aero_N)

classmethod from_yaml(car_yaml='parameters/car.yaml', setup_yaml='parameters/vehicle_setup.yaml')[source]#

Load both YAML files and construct an IrlCar.

Parameters:
  • car_yaml (str)

  • setup_yaml (str)

Return type:

IrlCar

heave_to_force(heave_m, axle)[source]#

Heave displacement (m) -> vertical spring force (N).

F = heave_stiffness * heave_m

Parameters:
  • heave_m (float | ndarray)

  • axle (str)

Return type:

float | ndarray

lateral_weight_transfer(ay_g)[source]#

Lateral weight transfer per axle.

Combines elastic (roll stiffness distribution) and geometric (direct load path through CG) components.

Parameters:

ay_g (float or array) – Lateral acceleration in g’s (positive = turning left).

Returns:

Weight transfer at the front and rear axles in Newtons. Positive values indicate load transfer to the outside wheel.

Return type:

(front_wt_N, rear_wt_N)

left_tire_angle(sw_deg)[source]#

Left tire steer angle via ackermann symmetry: -f(-x).

Parameters:

sw_deg (float | ndarray)

Return type:

ndarray

longitudinal_weight_transfer(ax_g)[source]#

Longitudinal weight transfer on the front axle.

Parameters:

ax_g (float or array) – Longitudinal acceleration in g’s (positive = accelerating forward, negative = braking).

Returns:

Change in front axle normal force (N). Negative under acceleration (rear loads up), positive under braking (front loads up). Anti-dive/squat percentages reduce the geometric weight transfer through the springs.

Return type:

float or np.ndarray

pot_to_force(pot_mm, axle)[source]#

Shock pot displacement (mm) -> vertical spring force (N).

Combines pot_to_heave() and heave_to_force().

Parameters:
  • pot_mm (float | ndarray)

  • axle (str)

Return type:

float | ndarray

pot_to_heave(pot_mm, axle)[source]#

Shock pot displacement (mm) -> chassis heave displacement (m).

heave_m = pot_mm / 1000 * MR

Parameters:
  • pot_mm (float | ndarray)

  • axle (str)

Return type:

float | ndarray

right_tire_angle(sw_deg)[source]#

Right tire steer angle from the ackermann curve.

Parameters:

sw_deg (float or array) – Steering wheel angle in degrees. Positive = right turn (right tire is inner, steers more); negative = left turn (right tire is outer, steers less).

Return type:

ndarray

roll_stiffness_distribution()[source]#

Front roll stiffness as a fraction of total (0-1).

Return type:

float

roll_stiffness_total()[source]#

Total roll stiffness (front + rear) in Nm/rad.

Return type:

float

tire_angles(sw_deg)[source]#

Return (left_deg, right_deg) for a given steering input.

Parameters:

sw_deg (float | ndarray)

Return type:

tuple[ndarray, ndarray]

suboptimumg.loganalysis.align_laps(lap_dfs, baseline_lap=None, sf_line=None, pos_cols=('posN', 'posE'), time_col='time_s')[source]#

Align all laps to a baseline via translation-only optimisation.

Uses cKDTree + Nelder-Mead to minimise mean log-nearest-neighbour distance after a rigid translation (no rotation, preserving yaw).

If sf_line is given, laps are re-trimmed at the S/F line after alignment.

Parameters:
  • lap_dfs (dict[int, DataFrame]) – As returned by split_laps().

  • baseline_lap (int or None) – Lap number to align to. None picks the fastest (shortest LapTime).

  • sf_line (((n1,e1), (n2,e2)) or None) – S/F line for re-trimming.

  • pos_cols (str)

  • time_col (str)

Returns:

Aligned (and optionally trimmed) lap DataFrames.

Return type:

dict[int, pd.DataFrame]

suboptimumg.loganalysis.build_comparison_df(lapsim_results, track, lap_df, log_columns=None, dist_col='LapDist')[source]#

Merge QSS results and resampled log data into one DataFrame.

The result is indexed by distance and contains both qss.* columns and the original log column names side by side.

Parameters:
  • lapsim_results (LapsimResults)

  • track (Track)

  • lap_df (pd.DataFrame)

  • log_columns (list[str] | None)

  • dist_col (str)

Return type:

pd.DataFrame

suboptimumg.loganalysis.build_qss_track(lap_df, dx=0.1, curvature_col='body.curvature', velocity_col='groundSpeed', pos_n_col='posN', pos_e_col='posE', dist_col='LapDist', cutoff_freq=0.18, max_radius=300.0, show_plots=False)[source]#

Build a QSS-compatible Track from a trimmed lap DataFrame.

Pipeline: resample to uniform dx -> FFT spatial low-pass on curvature -> convert to absolute radius -> fit B-spline on XY -> construct Track.

Parameters:
  • lap_df (DataFrame) – Single-lap DataFrame from split_laps() (must have dist_col, curvature_col, velocity_col, position cols).

  • dx (float) – Distance step in meters (default 0.1).

  • cutoff_freq (float) – Spatial frequency cutoff for curvature filter (1/m).

  • max_radius (float) – Radius clamp for near-straight segments.

  • show_plots (bool) – If True, display verification plots.

  • curvature_col (str)

  • velocity_col (str)

  • pos_n_col (str)

  • pos_e_col (str)

  • dist_col (str)

Return type:

Track

suboptimumg.loganalysis.compute_accelerations(df, vel_body='pcm.vnav.velocityBody.x', curvature_body='body.curvature', vel_track='track.velocity.x', curvature_track='track.curvature', out_body='body.accLat', out_track='track.accLat')[source]#

Compute lateral acceleration from v^2 * curvature.

Computes both body and track frames by default. All column name arguments have defaults matching upstream function outputs.

Parameters:
  • df (DataFrame)

  • vel_body (str)

  • curvature_body (str)

  • vel_track (str)

  • curvature_track (str)

  • out_body (str)

  • out_track (str)

Return type:

DataFrame

suboptimumg.loganalysis.compute_bicycle_steer_angle(df, irl_car, sw_col='ludwig.steeringWheel.angle', out_left='front.steerAngle.left', out_right='front.steerAngle.right', out_avg='front.steerAngle.bicycle')[source]#

Compute front tire steer angles from the steering wheel sensor.

Uses the Ackermann polynomial fit in irl_car to convert the steering wheel angle to individual left/right tire angles, then averages them for the bicycle-model front axle steer angle.

All outputs are in degrees.

Parameters:
  • df (pd.DataFrame)

  • irl_car (IrlCar)

  • sw_col (str)

  • out_left (str)

  • out_right (str)

  • out_avg (str)

Return type:

pd.DataFrame

suboptimumg.loganalysis.compute_body_slip_angle(df, track_heading_col='track.heading', yaw_col='pcm.vnav.yawPitchRoll.yaw', vel_body_col='pcm.vnav.velocityBody.x', low_speed_thresh=3.0, out_col='body.slipAngle')[source]#

Compute body slip angle (beta) from track heading and INS yaw.

Standard SAE sideslip: beta = track_heading - yaw, wrapped to +/-180. Positive beta = velocity vector to the right of the nose. Zeroed below low_speed_thresh where heading is unreliable.

Parameters:
  • df (DataFrame)

  • track_heading_col (str)

  • yaw_col (str)

  • vel_body_col (str)

  • low_speed_thresh (float)

  • out_col (str)

Return type:

DataFrame

suboptimumg.loganalysis.compute_cda(df, irl_car, vel_col='groundSpeed', time_col='time_s', rho=1.225, low_speed_thresh=5.0, out_col='aero.cda')[source]#

Estimate CDA from coastdown deceleration (free-rolling assumption).

F_drag = -m * a_x - F_rolling then CDA = F_drag / (0.5 * rho * v^2)

The user should trim the DataFrame to a coasting segment (no throttle, no braking) before calling this, or filter the output to regions where throttle and brake are zero.

Parameters:
  • df (DataFrame) – Must contain a velocity column and time index.

  • irl_car (IrlCar) – Provides mass, frontal area, and rolling resistance coefficient.

  • vel_col (str) – Velocity source (m/s).

  • rho (float) – Air density in kg/m^3.

  • low_speed_thresh (float) – Below this speed (m/s) CDA is zeroed.

  • out_col (str) – Output column name.

  • time_col (str)

Return type:

pd.DataFrame

suboptimumg.loganalysis.compute_cla(df, irl_car, ref_time_range, vel_col='groundSpeed', pot_fl='ludwig.shockpot.frontLeft', pot_fr='ludwig.shockpot.frontRight', pot_rl='ludwig.shockpot.rearLeft', pot_rr='ludwig.shockpot.rearRight', ax_col='pcm.vnav.linearAccelBody.x', rho=1.225, low_speed_thresh=5.0, out_cla='aero.cla', out_cop='aero.cop')[source]#

Estimate CLA and aero CoP from shock pot compression.

A reference time range defines the “zero-aero” baseline (e.g. car sitting still). At each timestep, the change in average axle pot compression is converted to vertical force via the IrlCar suspension math, and the aero contribution is isolated by subtracting longitudinal weight transfer through the springs.

Parameters:
  • df (DataFrame) – Must contain shock pot columns, a velocity column, and a longitudinal acceleration column.

  • irl_car (IrlCar) – Provides mass, frontal area, heave stiffness, motion ratios, and anti-dive/squat percentages.

  • ref_time_range ((t_start, t_end)) – Index (time_s) window where the car is stationary or at a known-zero-aero condition.

  • vel_col (str) – Velocity source for dynamic pressure.

  • rho (float) – Air density in kg/m^3.

  • low_speed_thresh (float) – Below this speed (m/s) CLA and CoP are zeroed.

  • out_cla (str) – Output column names.

  • out_cop (str) – Output column names.

  • pot_fl (str)

  • pot_fr (str)

  • pot_rl (str)

  • pot_rr (str)

  • ax_col (str)

Return type:

pd.DataFrame

suboptimumg.loganalysis.compute_curvature(df, yaw_rate_col='pcm.vnav.compensatedAngularRate.z', vel_body_col='pcm.vnav.velocityBody.x', track_heading_col='track.heading', vel_track_col='track.velocity.x', time_col='time_s', low_speed_thresh=3.0, median_window=8, radius_clip=500.0, out_body_curv='body.curvature', out_body_radius='body.radius', out_track_curv='track.curvature', out_track_radius='track.radius')[source]#

Compute path curvature and radius in body and track frames.

Body frame uses the measured yaw rate directly. Track frame differentiates the track heading. Both apply a rolling median and radius clipping.

Parameters:
  • df (DataFrame)

  • yaw_rate_col (str)

  • vel_body_col (str)

  • track_heading_col (str)

  • vel_track_col (str)

  • time_col (str)

  • low_speed_thresh (float)

  • median_window (int)

  • radius_clip (float)

  • out_body_curv (str)

  • out_body_radius (str)

  • out_track_curv (str)

  • out_track_radius (str)

Return type:

DataFrame

suboptimumg.loganalysis.compute_elapsed_distance(dfs, pos_n_col='posN', pos_e_col='posE', out_col='distance')[source]#

Cumulative arc-length distance from local cartesian position columns.

Parameters:
  • dfs (DataFrame or list[DataFrame])

  • pos_n_col (str) – Local cartesian position column names (must already exist).

  • pos_e_col (str) – Local cartesian position column names (must already exist).

  • out_col (str) – Name of the new distance column.

Return type:

Same type as input, with out_col added.

suboptimumg.loganalysis.compute_groundspeed(df, fl_col='pcm.wheelSpeeds.frontLeft', fr_col='pcm.wheelSpeeds.frontRight', out_col='groundSpeed')[source]#

Average front wheel speeds to estimate ground speed.

Uses undriven (front) wheels to avoid slip contamination from the driven (rear) axle.

Parameters:
  • df (DataFrame)

  • fl_col (str)

  • fr_col (str)

  • out_col (str)

Return type:

DataFrame

suboptimumg.loganalysis.compute_kinematic_steer_angle(df, curvature_body='body.curvature', curvature_track='track.curvature', wheelbase=None, car_yaml='parameters/car.yaml', out_body='body.steerAngle', out_track='track.steerAngle')[source]#

Compute kinematic steer angle from curvature (bicycle model).

delta = atan(wheelbase * curvature) in degrees. Wheelbase is auto-loaded from car.yaml if not specified.

Parameters:
  • df (DataFrame)

  • curvature_body (str)

  • curvature_track (str)

  • wheelbase (float | None)

  • car_yaml (str)

  • out_body (str)

  • out_track (str)

Return type:

DataFrame

suboptimumg.loganalysis.compute_rear_slip_ratio(df, wheel_speed_col='pcm.moc.motor.wheelSpeed', ground_speed_col='groundSpeed', out_col='rear.slipRatio', low_speed_thresh=1.0)[source]#

Compute rear axle longitudinal slip ratio.

slip = (v_wheel - v_ground) / v_ground

Positive = driven wheels spinning faster (traction loss). Zeroed below low_speed_thresh to avoid division blow-up.

Parameters:
  • df (DataFrame)

  • wheel_speed_col (str)

  • ground_speed_col (str)

  • out_col (str)

  • low_speed_thresh (float)

Return type:

DataFrame

suboptimumg.loganalysis.compute_track_frame_velocities(df, vel_n_col='velN', vel_e_col='velE', yaw_col='pcm.vnav.yawPitchRoll.yaw', low_speed_thresh=3.0, out_vx='track.velocity.x', out_vy='track.velocity.y', out_heading='track.heading')[source]#

Compute track-frame (velocity-frame) velocities from NED components.

Track heading is the course angle (direction of travel), same convention as yaw (0 = North, 90 = East, wraps at +/-180). At low speed the heading falls back to INS yaw.

Parameters:
  • df (DataFrame)

  • vel_n_col (str)

  • vel_e_col (str)

  • yaw_col (str)

  • low_speed_thresh (float)

  • out_vx (str)

  • out_vy (str)

  • out_heading (str)

Return type:

DataFrame

suboptimumg.loganalysis.convert_wheelspeeds_to_m_per_s(df, cols)[source]#

Convert wheel speed columns from mph to m/s.

Backs up original mph values into {col}_mph columns before overwriting with the converted values.

Parameters:
  • df (pd.DataFrame)

  • cols (list[str]) – Column names to convert.

Returns:

The same DataFrame, modified in-place and returned for chaining.

Return type:

pd.DataFrame

suboptimumg.loganalysis.correct_motor_data(df, rpm_col='pcm.moc.motor.angularSpeed', out_col='pcm.moc.motor.wheelSpeed', gear_ratio=None, tire_radius_in=None, car_yaml='parameters/car.yaml')[source]#

Flip motor RPM sign and compute driven wheel speed.

The inverter logs motor RPM as negative for forward motion. This function negates the RPM (so positive = forward) and computes the corresponding driven wheel linear speed in m/s.

Parameters:
  • df (pd.DataFrame)

  • rpm_col (str) – Motor angular speed column (RPM, raw sign: negative = forward).

  • out_col (str) – Column name for the computed wheel speed (m/s).

  • gear_ratio (float or None) – Overall gear ratio (motor RPM / wheel RPM). If None, loaded from car_yaml (pwrtn.ratio).

  • tire_radius_in (float or None) – Loaded tire radius in inches. If None, loaded from car_yaml (tires.tire_radius).

  • car_yaml (str) – Path to car YAML file, used when gear_ratio or tire_radius_in are not provided.

Returns:

Modified in-place and returned for chaining.

Return type:

pd.DataFrame

suboptimumg.loganalysis.derive_quantities(df)[source]#

Compute all standard derived quantities.

TODO: Wire up the full set of derived.py calls.

Parameters:

df (DataFrame)

Return type:

DataFrame

suboptimumg.loganalysis.detect_lap_crossings(df, sf_line, time_col='time_s', pos_cols=('posN', 'posE'), min_lap_time_s=20.0, start_time=None, end_time=None)[source]#

Detect start/finish line crossings and label laps.

Works in local cartesian coordinates (posN, posE). The sf_line is a pair of (N, E) tuples defining the S/F segment.

If sf_line contains lat/lon tuples instead, convert them first with gps_to_local_cartesian and pick coordinates from the map plot.

Parameters:
  • df (pd.DataFrame) – Must contain pos_cols and time_col.

  • sf_line (((n1, e1), (n2, e2))) – Start/finish line endpoints in local cartesian.

  • time_col (str)

  • pos_cols ((str, str)) – (North column, East column).

  • min_lap_time_s (float) – Minimum time between crossings to accept as a real lap.

  • start_time (float or None) – Time window to search within. None uses full range.

  • end_time (float or None) – Time window to search within. None uses full range.

Returns:

Copy of df with a Lap column added (0 = before first crossing, 1 = first lap, etc.).

Return type:

pd.DataFrame

suboptimumg.loganalysis.dump_variables(analyzer)[source]#

Enumerate every variable in a loaded PERDA log.

Variable IDs are intentionally excluded because they are log-internal and must not be used for access.

Parameters:

analyzer (Analyzer) – A loaded PERDA Analyzer instance.

Returns:

Columns: cpp_name, num_points, start_time, end_time, duration_s, min_value, max_value, median_freq_hz.

Return type:

pd.DataFrame

suboptimumg.loganalysis.fft_group(df, partial_path, log_y=False, stacked=False, **kwargs)[source]#

Plot FFT for all columns matching partial_path.*.

Default is linear y-axis and overlaid traces.

Parameters:
  • df (pd.DataFrame)

  • partial_path (str)

  • log_y (bool)

  • stacked (bool)

Return type:

go.Figure

suboptimumg.loganalysis.filter_speeds(df, zscore_window=8.0, zscore_thresh=2.5, lowpass_hz=3.0)[source]#

Z-score + lowpass pipeline for wheel speed signals.

  1. Z-score filter the four raw wheelspeed sensors.

  2. Recompute groundSpeed from the cleaned front wheelspeeds.

  3. Lowpass all wheelspeeds, motor wheelSpeed, and groundSpeed.

Parameters:
  • df (DataFrame)

  • zscore_window (float)

  • zscore_thresh (float)

  • lowpass_hz (float)

Return type:

DataFrame

suboptimumg.loganalysis.gps_to_local_cartesian(dfs, lat_col='pcm.vnav.posLla.latitude', lon_col='pcm.vnav.posLla.longitude', origin=None)[source]#

Equirectangular projection from lat/lon to local North/East (meters).

Adds posN and posE columns. If origin is None, uses the first data point of the first DataFrame as the reference.

Parameters:
  • dfs (DataFrame or list[DataFrame])

  • lat_col (str)

  • lon_col (str)

  • origin ((lat_deg, lon_deg) or None)

Return type:

Same type as input, with posN and posE added.

suboptimumg.loganalysis.intake_vd_starter_kit(analyzer, deduplicate=True, resample_method='linear')[source]#

Load the standard vehicle-dynamics variable set into a DataFrame.

Wraps perda_to_dataframe with the default VD variable list and ZOH deduplication on VectorNav position and velocity channels.

pcm.vnav.gpsFix is always resampled with zero-order hold (it is a discrete state, not a continuous signal — linear interpolation between 0 and 3 would produce nonsensical fractional fix values that defeat downstream gpsFix-based masking).

Sentinel 0.0 samples in posLla.latitude/.longitude (VectorNav’s “no INS solution” marker) are stripped from the source DataInstance before resampling so dedup + linear interpolation don’t smear them into ramps across no-fix gaps.

Parameters:
  • analyzer (Analyzer)

  • deduplicate (bool)

  • resample_method (str | dict[str, str])

Return type:

pd.DataFrame

suboptimumg.loganalysis.load_vehicle_setup(path='parameters/vehicle_setup.yaml')[source]#

Load a vehicle_setup.yaml configuration file.

Any YAML mapping containing both data (list of [x, y] pairs) and poly_order (int) is automatically converted to a FittedCurve. Everything else passes through as plain Python types.

Parameters:

path (str or Path) – Path to the YAML file.

Returns:

Parsed YAML contents with curve entries as FittedCurve objects. Typical top-level keys: alignment, steering, suspension.

Return type:

dict

suboptimumg.loganalysis.logging_frequencies(analyzer, var_names)[source]#

Compute sample-rate statistics for a list of variables.

Parameters:
  • analyzer (Analyzer) – A loaded PERDA Analyzer instance.

  • var_names (list[str]) – CAN variable names (cpp_name) to analyse.

Returns:

Columns: var_name, median_freq_hz, mean_freq_hz, min_freq_hz, max_freq_hz, num_samples, num_gaps. num_gaps counts intervals exceeding 3x the median interval, indicating dropped samples.

Return type:

pd.DataFrame

suboptimumg.loganalysis.lowpass_filter(dfs, columns, cutoff_hz, time_col='time_s', order=4)[source]#

Apply a Butterworth lowpass filter to one or more columns.

On first call for a given column the current data is saved to <col>_pre_lp. Subsequent calls always filter from <col>_pre_lp so that re-filtering at a different cutoff does not stack, while still preserving any upstream filtering (e.g. zscore_filter) that was applied before this function.

A <col>_original backup is also created on the very first filter call (zscore or lowpass) to allow full reset via undo_filters().

Parameters:
  • dfs (DataFrame or list[DataFrame]) – Accepts a single DataFrame or a list; applies to all.

  • columns (str or list[str]) – Column name(s) to filter.

  • cutoff_hz (float) – Cutoff frequency in Hz.

  • time_col (str) – Time column to derive sampling frequency from.

  • order (int) – Butterworth filter order (applied forward-backward, so effective order is 2x).

Returns:

  • Same type as *dfs, with filtered columns overwritten and*

  • _pre_lp backups created on first filter.

Return type:

DataFrame | list[DataFrame]

suboptimumg.loganalysis.lowpass_filter_by_distance(dfs, columns, cutoff_freq_per_meter, distance_col='distance', order=4)[source]#

Apply a Butterworth lowpass in the spatial domain (1/m).

Same semantics as lowpass_filter() but the sample rate is derived from a distance column instead of time.

Parameters:
  • dfs (DataFrame or list[DataFrame])

  • columns (str or list[str])

  • cutoff_freq_per_meter (float) – Cutoff in cycles per meter (1/m).

  • distance_col (str)

  • order (int)

Return type:

Same type as dfs.

suboptimumg.loganalysis.lowpass_group(df, partial_path, cutoff_hz, **kwargs)[source]#

Lowpass filter all columns matching partial_path.*.

Parameters:
  • df (DataFrame)

  • partial_path (str)

  • cutoff_hz (float)

Return type:

DataFrame

suboptimumg.loganalysis.measure_understeer_gradient(df, time_range, irl_car, test_type='constant_steer', min_ay_g=0.1, force_zero_intercept=False, fit_mode='linear', symmetric=False, show_ramp=False, overlay_diagnostics=False, sw_col='ludwig.steeringWheel.angle', ay_col='body.accLat', kinematic_col='body.steerAngle', bicycle_col='front.steerAngle.bicycle', vel_col='groundSpeed')[source]#

Measure the understeer gradient K from a steady-state test window.

Computes understeer_angle = |delta_bicycle| - |delta_kinematic| and fits a line against |a_y| in g’s. The slope is the understeer gradient K (deg/g). K > 0 = understeer, K < 0 = oversteer.

Works for both constant-speed/increasing-steer and constant-steer/increasing-speed test procedures — the core math is the same; test_type only affects the diagnostic subplot.

Parameters:
  • df (DataFrame) – Must contain the columns referenced by the *_col arguments. The DataFrame is not modified.

  • time_range ((t_start, t_end)) – time_s window to analyse.

  • irl_car (IrlCar) – Used only for display / future extensions.

  • test_type (str) – "constant_steer" or "constant_speed". Controls which variable is shown in the diagnostic subplot.

  • min_ay_g (float) – Minimum lateral acceleration (g) to include in the fit. Filters out low-speed noise.

  • force_zero_intercept (bool) – If True, force the fit through the origin (intercept = 0). Useful when a systematic offset (e.g. steering sensor zero error) is inflating the intercept.

  • fit_mode (str) – "linear" (default) — classic K = slope of UG vs a_y. "quadratic" — fits UG = c2 * a_y^2 + c1 * a_y + c0. "cubic" — fits UG = c3 * a_y^3 + c2 * a_y^2 + c1 * a_y + c0. For non-linear modes the reported K is the local slope at a_y = 0 (the c1 term); higher-order terms capture progressive under/oversteer with rising lateral load.

  • symmetric (bool) – If True, force the fitted equation to be symmetric across the y-axis (f(-x) = f(x)) by zeroing all odd-degree terms. Quadratic becomes c2 * a_y^2 + c0; cubic likewise reduces to even-only terms. Combined with force_zero_intercept the quadratic/cubic fits collapse to c2 * a_y^2. Linear + symmetric is degenerate (constant only) and not allowed.

  • show_ramp (bool) – If True, add a subplot for the ramped variable (speed for constant_steer, steering angle for constant_speed).

  • overlay_diagnostics (bool) – If True and show_ramp is True, overlay both diagnostic traces on a single subplot with dual y-axes instead of separate subplots.

  • sw_col (str) – Column names in df.

  • ay_col (str) – Column names in df.

  • kinematic_col (str) – Column names in df.

  • bicycle_col (str) – Column names in df.

  • vel_col (str) – Column names in df.

Returns:

Multi-panel figure: scatter + fit (top), diagnostic time-series (bottom panel(s)).

Return type:

go.Figure

suboptimumg.loganalysis.patch_ned_velocity(df, vel_x_col='pcm.vnav.velocityBody.x', vel_y_col='pcm.vnav.velocityBody.y', vel_z_col='pcm.vnav.velocityBody.z', yaw_col='pcm.vnav.yawPitchRoll.yaw', ned_cols=('velN', 'velE', 'velD'))[source]#

Correct mislabeled NED velocities that were logged as body-frame.

Due to a VectorNav configuration issue, velocityBody.x/y/z actually contains NED (North/East/Down) velocities rather than body-frame (Forward/Right/Down) velocities. This function:

  1. Copies the raw NED data into new columns (ned_cols).

  2. Rotates NED → FRD body frame using yaw and overwrites the original velocityBody columns with corrected values.

The rotation (yaw-only, ignoring pitch/roll for ground vehicles):

v_fwd   =  velN * cos(yaw) + velE * sin(yaw)
v_right = -velN * sin(yaw) + velE * cos(yaw)
v_down  =  velD            (unchanged)

This is a temporary workaround. When the VectorNav is reconfigured to output body-frame velocities natively, skip this step entirely.

Parameters:
  • df (pd.DataFrame) – DataFrame with velocity and yaw columns (typically from perda_to_dataframe).

  • vel_x_col (str) – The mislabeled velocity columns (actually NED).

  • vel_y_col (str) – The mislabeled velocity columns (actually NED).

  • vel_z_col (str) – The mislabeled velocity columns (actually NED).

  • yaw_col (str) – Heading column in degrees (0 = North, 90 = East, CW positive).

  • ned_cols (tuple[str, str, str]) – Names for the preserved NED copy columns.

Returns:

The same DataFrame, modified in-place and returned for chaining.

Return type:

pd.DataFrame

suboptimumg.loganalysis.perda_load_folder(folder, extra_vars=None, deduplicate=True, lat_var='pcm.vnav.posLla.latitude', lon_var='pcm.vnav.posLla.longitude', gps_fix_var='pcm.vnav.gpsFix', min_gps_fix=1.0)[source]#

Batch-load GPS data from all PER CSV logs using PERDA.

Uses PERDA’s Analyzer for CSV parsing instead of the custom lightweight parser in quick_load_gps().

Parameters:
  • folder (str or Path) – Directory containing .csv log files.

  • extra_vars (list[str] or None) – Full CAN variable names to extract alongside GPS.

  • deduplicate (bool) – Collapse consecutive duplicate GPS positions.

  • lat_var (str) – Full CAN variable names for latitude and longitude.

  • lon_var (str) – Full CAN variable names for latitude and longitude.

  • gps_fix_var (str) – CAN variable name for GPS fix status.

  • min_gps_fix (float) – Minimum GPS fix value to keep.

Returns:

Keyed by filename (not full path). Logs with no GPS lock are omitted.

Return type:

dict[str, pd.DataFrame]

suboptimumg.loganalysis.perda_load_gps(path, lat_var='pcm.vnav.posLla.latitude', lon_var='pcm.vnav.posLla.longitude', gps_fix_var='pcm.vnav.gpsFix', min_gps_fix=1.0, extra_vars=None, deduplicate=True)[source]#

Load GPS data from a PER CSV log using PERDA’s Analyzer.

Parameters:
  • path (str or Path) – Path to a PER-format .csv log file.

  • lat_var (str) – Full CAN variable names for latitude and longitude.

  • lon_var (str) – Full CAN variable names for latitude and longitude.

  • gps_fix_var (str) – CAN variable name for GPS fix status.

  • min_gps_fix (float) – Minimum GPS fix value to keep (0 = no fix, 3 = 3D fix).

  • extra_vars (list[str] or None) – Additional CAN variables to extract alongside GPS.

  • deduplicate (bool) – Collapse consecutive duplicate GPS positions to remove ZOH-inflated samples (GPS updates at ~4 Hz but is logged at CAN rate ~100 Hz).

Returns:

DataFrame indexed by time_s with latitude, longitude columns (plus extras). Returns None if the file has no usable GPS data.

Return type:

pd.DataFrame or None

suboptimumg.loganalysis.perda_to_dataframe(analyzer, var_names, resample_method='linear', resample_freq_hz=None, reference_var=None, deduplicate_vars=None)[source]#

Convert selected PERDA variables into a single aligned DataFrame.

Only loads the explicitly requested variables — never touches the full set of 200-300+ variables in a log.

Parameters:
  • analyzer (Analyzer) – A loaded PERDA Analyzer instance.

  • var_names (list[str]) – CAN names of the variables to extract.

  • resample_method (str or dict[str, str]) – Interpolation method for aligning to a common time grid. "linear" (default), "zoh" (zero-order hold / persist until update), "nearest", or "cubic". Pass a dict mapping variable names to methods to mix strategies.

  • resample_freq_hz (float or None) – If given, resample to a uniform grid at this frequency (Hz). Otherwise the union of all source timestamps is used.

  • reference_var (str or None) – Left-join all variables to this variable’s timestamp grid. If None and resample_freq_hz is also None, the variable with the most samples is chosen automatically.

  • deduplicate_vars (list[str] or None) – Variable names whose consecutive duplicate (ZOH) values should be collapsed before resampling. Useful for signals like GPS position that are logged at a high CAN rate but only update at a lower real rate (e.g. 100 Hz CAN / 4 Hz real). Deduplication keeps the first sample of each constant run so that resampling interpolates smoothly between real updates.

Returns:

Index: time_s (float64 seconds, zero-based from log start). One column per variable.

Return type:

pd.DataFrame

suboptimumg.loganalysis.plot_comparison_trace(laps, variable='groundSpeed', qss_df=None, baseline='fastest', dist_col='LapDist', vel_col='groundSpeed', units='m/s', dx=0.1)[source]#

Multi-lap comparison trace with cumulative time-delta subplot.

Parameters:
  • laps (dict[label, DataFrame]) – Trimmed lap DataFrames from split_laps().

  • variable (str) – Column to plot on the top subplot (log-world name).

  • qss_df (DataFrame or None) – If provided, the QSS series is included. Column names are auto-resolved via LOG_TO_QSS.

  • baseline (str or int) – Reference for time delta. "fastest" picks the lap with the shortest LapTime. "first" / "last" use positional order. "qss" uses the QSS as baseline. An explicit key selects that lap.

  • vel_col (str) – Velocity column used for time-delta computation (log-world name, auto-resolved for QSS).

  • dist_col (str)

  • units (str)

  • dx (float)

Return type:

Figure

suboptimumg.loganalysis.plot_fft(df, columns, domain='time', time_col='time_s', distance_col='distance', stacked=True, cutoff=None, log_x=True, log_y=False, xlim=None)[source]#

Plot FFT magnitude spectrum of one or more columns.

Parameters:
  • df (pd.DataFrame)

  • columns (str or list[str])

  • domain (str) – "time" or "distance".

  • time_col (str)

  • distance_col (str)

  • stacked (bool) – True -> one subplot per column; False -> all on one plot.

  • cutoff (float or None) – If given, draw a vertical dashed line at this frequency.

  • log_x (bool) – Logarithmic x-axis.

  • log_y (bool) – Logarithmic y-axis.

  • xlim ((start, end) or None) – Restrict the FFT to a subdomain of the x-axis (time in seconds or distance in meters, depending on domain). None uses the full range.

Return type:

go.Figure

Note

No max_display_hz downsampling is applied here — the FFT output is already in the frequency domain and thinning would corrupt the spectrum shape.

suboptimumg.loganalysis.plot_gps_trajectory(df, lat_col='pcm.vnav.posLla.latitude', lon_col='pcm.vnav.posLla.longitude', time_col='time_s', color_by='time', max_display_hz=100.0, width=700, height=700)[source]#

Interactive 2D GPS trajectory plot with time-based trim sliders.

Parameters:
  • df (pd.DataFrame)

  • lat_col (str)

  • lon_col (str)

  • time_col (str) – Column used for slider values and colour mapping.

  • color_by (str) – "time" colours by time_col.

  • max_display_hz (float) – Target sample rate for display. Data is naively thinned (every Nth point) to approximately this rate. The source DataFrame is not modified.

  • width (int)

  • height (int)

Returns:

fig is the FigureWidget, widget_box is the VBox with sliders that can be display()-ed.

Return type:

(fig, widget_box)

suboptimumg.loganalysis.plot_group(df, partial_path, stacked=False, **kwargs)[source]#

Plot all columns matching partial_path.*.

Default is overlaid traces on a single axis.

Parameters:
  • df (pd.DataFrame)

  • partial_path (str)

  • stacked (bool)

Return type:

go.Figure

suboptimumg.loganalysis.plot_heatmap(baseline_df, comparison_df, variable='groundSpeed', baseline_label='Baseline', comparison_label='Comparison', pos_cols=('posN', 'posE'), dist_col='LapDist', units='m/s', dx=0.1)[source]#

3-panel 2D track heatmap comparing any two DataFrames.

Works for lap-vs-lap or lap-vs-QSS. Column names are auto-resolved via LOG_TO_QSS so the caller always uses log-world names (e.g. "groundSpeed").

Parameters:
  • baseline_df (DataFrame) – Any two lap DataFrames, or a lap and a qss_df.

  • comparison_df (DataFrame) – Any two lap DataFrames, or a lap and a qss_df.

  • variable (str) – Column to compare (log-world name).

  • pos_cols ((north_col, east_col)) – Position columns (log-world names).

  • units (str) – Label for colorbars and hover.

  • dx (float) – Resample step when the two DataFrames are on different grids.

  • baseline_label (str)

  • comparison_label (str)

  • dist_col (str)

Return type:

Figure

suboptimumg.loganalysis.plot_lap_overlay(lap_dfs, baseline_lap=None, sf_line=None, pos_cols=('posN', 'posE'), time_col='time_s', max_display_hz=100.0)[source]#

Overlay all laps on a 2D scatter plot.

Parameters:
  • lap_dfs (dict[int, DataFrame])

  • baseline_lap (int or None)

  • sf_line (S/F line endpoints or None)

  • pos_cols ((north_col, east_col))

  • time_col (str) – Time column used to estimate sample rate for downsampling.

  • max_display_hz (float) – Target display sample rate. 0 or None disables.

Return type:

go.Figure

suboptimumg.loganalysis.plot_log(df, columns, stacked=True, domain='time', time_col='time_s', distance_col='distance', title=None, height_per_subplot=250, max_display_hz=100.0)[source]#

General-purpose log plotting function.

Parameters:
  • df (pd.DataFrame) – The log DataFrame.

  • columns (list[str]) – Column names to plot.

  • stacked (bool) – True -> one subplot per column (shared x-axis). False -> all traces on a single plot.

  • domain (str) – "time" -> x-axis is time_col. "distance" -> x-axis is distance_col.

  • time_col (str) – Column name for time. If the DataFrame index is named time_s, it is used automatically when time_col is not found as a column.

  • distance_col (str) – Column name for distance.

  • title (str or None) – Figure title. Defaults to a generated summary.

  • height_per_subplot (int) – Pixel height per subplot row (only used when stacked=True).

  • max_display_hz (float) – Target display sample rate for naive downsampling. 0 or None disables downsampling.

Return type:

go.Figure

suboptimumg.loganalysis.plot_velocity_heatmap(comp_df, track, log_vel_col='groundSpeed', qss_vel_col='qss.velocity')[source]#

3-panel velocity heatmap (IRL vs QSS).

Thin wrapper around plot_heatmap() for backward compatibility.

Parameters:
  • comp_df (DataFrame)

  • track (Track)

  • log_vel_col (str)

  • qss_vel_col (str)

Return type:

Figure

suboptimumg.loganalysis.plot_velocity_trace(comp_df, log_vel_col='groundSpeed', qss_vel_col='qss.velocity')[source]#

F1-style velocity trace (IRL vs QSS).

Thin wrapper around plot_comparison_trace() for backward compatibility.

Parameters:
  • comp_df (DataFrame)

  • log_vel_col (str)

  • qss_vel_col (str)

Return type:

Figure

suboptimumg.loganalysis.prepare_gps_data(df, lat_col='latitude', lon_col='longitude', max_radius_m=5000.0, max_jump_m=30.0)[source]#

Full GPS cleaning pipeline: filter invalid positions, project to local cartesian, and remove spatial outliers.

Steps applied in order:

  1. Drop rows where lat/lon are zero or outside physical range (|lat| > 90 or |lon| > 180).

  2. Equirectangular projection to local East/North meters, with the median lat/lon as origin.

  3. Drop points farther than max_radius_m from the origin (warns if any are removed).

  4. Drop points that jump more than max_jump_m from their predecessor (warns if any are removed).

ZOH deduplication of stale GPS samples should be handled upstream via perda_to_dataframe(deduplicate_vars=...).

Adds posE (East, meters) and posN (North, meters) columns to the returned DataFrame.

Parameters:
  • df (pd.DataFrame) – Must contain lat_col and lon_col columns.

  • lat_col (str) – Column names for latitude and longitude in degrees.

  • lon_col (str) – Column names for latitude and longitude in degrees.

  • max_radius_m (float) – Maximum distance from the median position to keep.

  • max_jump_m (float) – Maximum point-to-point distance to keep.

Returns:

Cleaned copy with posE and posN columns added and invalid rows removed.

Return type:

pd.DataFrame

suboptimumg.loganalysis.preprocess_gps(df)[source]#

Resolve GPS columns, clean/project, and compute elapsed distance.

Parameters:

df (DataFrame)

Return type:

DataFrame

suboptimumg.loganalysis.preprocess_speeds(df, patch_ned=True, car_yaml='parameters/car.yaml')[source]#

NED velocity patch, wheelspeed unit conversion, motor correction, ground speed.

Parameters:
  • df (DataFrame)

  • patch_ned (bool)

  • car_yaml (str)

Return type:

DataFrame

suboptimumg.loganalysis.qss_results_to_dataframe(lapsim_results, track)[source]#

Convert QSS lapsim output arrays into a distance-indexed DataFrame.

Columns use a qss. prefix so they sit cleanly alongside log columns in a merged comparison DataFrame. Includes position columns (qss.posN, qss.posE) from the track.

Parameters:
Return type:

pd.DataFrame

suboptimumg.loganalysis.quick_load_folder(folder, lat_pattern='posLla.latitude', lon_pattern='posLla.longitude', extra_patterns=None, parallel=False, max_workers=None)[source]#

Batch-load GPS previews for all PER CSV logs in a folder.

Parameters:
  • folder (str or Path) – Directory containing .csv log files.

  • lat_pattern (str) – Passed to quick_load_gps().

  • lon_pattern (str) – Passed to quick_load_gps().

  • extra_patterns (list[str] or None) – Passed to quick_load_gps().

  • parallel (bool) – If True, load files in parallel using ProcessPoolExecutor.

  • max_workers (int or None) – Max worker processes when parallel=True. Defaults to os.cpu_count() - 2 (minimum 1).

Returns:

Keyed by filename (not full path). Logs with no GPS lock are omitted.

Return type:

dict[str, pd.DataFrame]

suboptimumg.loganalysis.quick_load_gps(path, lat_pattern='posLla.latitude', lon_pattern='posLla.longitude', gps_fix_pattern='gpsFix', min_gps_fix=1.0, extra_patterns=None, _verbose=True)[source]#

Fast GPS extractor that parses a PER CSV without PERDA.

Reads only the header to discover variable IDs, then streams the data section keeping only lat/lon rows. Typically 5-10x faster than a full PERDA load for GPS preview purposes.

Parameters:
  • path (str or Path) – Path to a PER-format .csv log file.

  • lat_pattern (str) – Substrings matched (case-sensitive) against the cpp_name in the header to identify latitude and longitude variable IDs.

  • lon_pattern (str) – Substrings matched (case-sensitive) against the cpp_name in the header to identify latitude and longitude variable IDs.

  • extra_patterns (list[str] or None) – Additional variable patterns to extract alongside lat/lon (e.g. ["gpsFix", "posLla.altitude"]).

  • gps_fix_pattern (str)

  • min_gps_fix (float)

  • _verbose (bool)

Returns:

DataFrame indexed by time_s with latitude, longitude columns (plus any extras). Returns None if the file has no GPS lock (all lat/lon values are zero).

Return type:

pd.DataFrame or None

suboptimumg.loganalysis.recompute_steering_angle(df, raw_col='ludwig.steeringWheel.raw', angle_col='ludwig.steeringWheel.angle', calibration=((1.86, -97.0), (2.93, 0.0), (3.96, 97.0)))[source]#

Regenerate ludwig.steeringWheel.angle from the raw analog voltage.

The on-vehicle DSP that produces steeringWheel.angle has been observed to drift relative to the raw pot signal. This function rebuilds the angle column from steeringWheel.raw using a least-squares polynomial fit through the calibration points (quadratic by default — exact fit for the standard 3-point left/center/right calibration, capturing the small nonlinearity of the pot).

Any existing angle_col is preserved as {angle_col}_original.

Parameters:
  • df (pd.DataFrame) – Must contain raw_col.

  • raw_col (str) – Raw analog steering voltage column (volts).

  • angle_col (str) – Output / overwritten angle column (degrees).

  • calibration (tuple of (voltage, angle) pairs) – Calibration points. With three points a quadratic fits all of them exactly; with more points it’s a least-squares quadratic. With two points it falls back to a linear fit.

Returns:

Modified in-place and returned for chaining.

Return type:

pd.DataFrame

suboptimumg.loganalysis.resample_log_to_distance(lap_df, distance_grid, columns=None, dist_col='LapDist')[source]#

Resample selected log columns onto a QSS distance grid.

Uses linear interpolation via numpy.interp().

Parameters:
  • lap_df (DataFrame) – Trimmed single-lap DataFrame.

  • distance_grid (ndarray) – Target distance array (from a built Track).

  • columns (list[str] or None) – Columns to resample. If None, uses all non-None values from QSS_TO_LOG.

  • dist_col (str) – Distance column in lap_df.

Return type:

DataFrame

suboptimumg.loganalysis.resolve_gps_columns(dfs, ins_lat_col='pcm.vnav.posLla.latitude', ins_lon_col='pcm.vnav.posLla.longitude', fix_col='pcm.vnav.gpsFix', out_lat_col='latitude', out_lon_col='longitude')[source]#

Build unified latitude/longitude columns with INS-lock fallback.

The VN-300 INS solution (posLla) reads zero when the INS hasn’t converged. This function creates clean latitude/longitude columns that mask out invalid (zero) samples and, when the INS lock is eventually acquired, use the INS-filtered position.

When fix_col is provided and present in the DataFrame, rows where gpsFix == 0 are also treated as invalid.

Invalid rows are set to NaN so downstream functions can handle them (e.g. drop, interpolate, or skip).

Parameters:
  • dfs (DataFrame or list[DataFrame])

  • ins_lat_col (str) – INS solution position columns (the only position variables in these CAN logs).

  • ins_lon_col (str) – INS solution position columns (the only position variables in these CAN logs).

  • fix_col (str or None) – GPS fix status column. None skips the fix check.

  • out_lat_col (str) – Names for the output columns.

  • out_lon_col (str) – Names for the output columns.

Return type:

Same type as input, with out_lat_col and out_lon_col added.

suboptimumg.loganalysis.sample_log(df, patterns=None, percentiles=None)[source]#

Print sampled values at key points through the log for debugging.

By default matches columns containing ‘pos’, ‘lat’, ‘latitude’, ‘lon’, ‘long’, or ‘longitude’ (case-insensitive).

Parameters:
  • df (pd.DataFrame) – The log DataFrame (output of perda_to_dataframe).

  • patterns (list[str] or None) – Substrings to match against column names.

  • percentiles (list[int] or None) – Which percentile indices to sample. Default: [0, 25, 50, 75, 100].

Return type:

None

suboptimumg.loganalysis.simulate_on_track(lap_df, track, car_yaml='../parameters/car.yaml', velocity_col='groundSpeed')[source]#

Load a car, simulate on a Track, and return a QSS DataFrame.

Parameters:
  • lap_df (DataFrame) – The trimmed single-lap DataFrame used to build the track.

  • track (Track) – A QSS-compatible Track (from build_qss_track()).

  • car_yaml (str) – Path to the car YAML config file.

  • velocity_col (str) – Column to read IRL initial speed from.

Returns:

sim is the CustomRun instance (carries push/coast results). qss_df is a distance-indexed DataFrame with qss.* columns.

Return type:

(sim, qss_df)

suboptimumg.loganalysis.split_laps(df, buffer_distance_m=5.0, min_lap_distance_m=20.0, pos_cols=('posN', 'posE'), time_col='time_s')[source]#

Split a DataFrame by Lap column with arc-length buffer.

Each lap DataFrame gets LapTime and LapDist columns added.

Parameters:
  • df (pd.DataFrame) – Must have Lap, pos_cols, and time_col columns.

  • buffer_distance_m (float) – Extra path distance before/after each lap boundary.

  • min_lap_distance_m (float) – Discard laps shorter than this.

  • pos_cols (str)

  • time_col (str)

Returns:

Keyed by lap number (>0).

Return type:

dict[int, pd.DataFrame]

suboptimumg.loganalysis.trim_dataframe(df, time_col, start_time, end_time)[source]#

Return rows where time_col is between start_time and end_time (inclusive).

Parameters:
  • df (DataFrame)

  • time_col (str)

  • start_time (float)

  • end_time (float)

Return type:

DataFrame

suboptimumg.loganalysis.undo_filters(df, columns)[source]#

Restore columns to their original (pre-filter) state.

Copies data back from <col>_original and removes all backup columns (_original, _pre_lp, _raw).

Parameters:
  • df (pd.DataFrame)

  • columns (str or list[str]) – Column name(s) to restore.

Returns:

The same DataFrame, modified in-place and returned.

Return type:

pd.DataFrame

suboptimumg.loganalysis.unwrap_degrees(df, columns, suffix='', discontinuity=180.0)[source]#

Unwrap angle columns (degrees) to remove +-180 discontinuities.

The VN-300 yaw output reads 0 at north, +90 east, +180 south, then jumps to -180 and climbs to 0. This creates step discontinuities that break derivatives and integrations. np.unwrap removes them by adding/subtracting 360 at each jump.

Parameters:
  • df (pd.DataFrame)

  • columns (str or list[str]) – Column name(s) to unwrap.

  • suffix (str) – If non-empty, the unwrapped result is written to <col><suffix> and the original column is kept. If empty (default), the column is overwritten in place.

  • discontinuity (float) – Maximum allowed step between consecutive samples (degrees). Jumps larger than this are treated as wraps.

Returns:

Same DataFrame with unwrapped column(s).

Return type:

pd.DataFrame

suboptimumg.loganalysis.zfilter_group(df, partial_path, window_s=2.0, threshold=4.8, **kwargs)[source]#

Z-score filter all columns matching partial_path.*.

Parameters:
  • df (DataFrame)

  • partial_path (str)

  • window_s (float)

  • threshold (float)

Return type:

DataFrame

suboptimumg.loganalysis.zscore_filter(dfs, columns, window_s=2.0, threshold=4.8, time_col='time_s')[source]#

Mask outliers using a rolling-window z-score.

For each sample, a z-score is computed relative to the local rolling mean and standard deviation. Samples exceeding threshold are replaced with NaN and linearly interpolated.

Always re-filters from <col>_original (the true intake data) so that re-running with different parameters does not stack. If a <col>_pre_lp backup exists (lowpass was already applied), it is updated to the new zscore output so subsequent lowpass calls chain correctly.

Parameters:
  • dfs (DataFrame or list[DataFrame])

  • columns (str or list[str]) – Column name(s) to filter.

  • window_s (float) – Rolling window size in seconds.

  • threshold (float) – Z-score threshold – samples with |z| > threshold are masked.

  • time_col (str) – Time column used to convert window_s to a sample count.

Returns:

  • Same type as *dfs, with outliers masked to NaN and*

  • _original backups created on first call.

Return type:

DataFrame | list[DataFrame]

Modules

corrections

Post-intake data corrections for known CAN log issues.

derived

Derived quantities computed from raw log data.

filtering

FFT plotting and digital lowpass filtering utilities.

fitted_curve

Polynomial curve fitting with Horner evaluation.

gps

GPS visualization, trimming, coordinate transforms, and distance computation.

intake

PERDA log -> aligned/resampled pandas DataFrame, plus fast GPS-only loader.

irl_car

IrlCar — real-world car wrapper combining Car + vehicle_setup parameters.

laps

Lap division, splitting, and alignment utilities.

macros

High-level convenience macros that chain multiple loganalysis steps.

plotting

Generalized time-series and distance-series logging/plotting.

track_builder

Build QSS-compatible Track objects from processed lap DataFrames.

understeer

Understeer gradient measurement from steady-state test data.

variables

Log introspection utilities: variable enumeration, frequency analysis, config loading.