Files
firestar5683 a82935f326 User interfaces and Android Auto
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: pharmacomaniac <blittle65@gmail.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

238 lines
11 KiB
Python

"""Bounded identification of an already-supported linear torque conversion.
Applied torque and bank-corrected, lag-aligned physical response identify only
latAccelFactor. Held-out model fit is not a prediction of closed-loop tracking.
Offset, friction, feedback gains, native limits and rich surfaces stay fixed.
"""
from collections.abc import Mapping
from dataclasses import dataclass
import hashlib
import json
import math
import statistics
from openpilot.starpilot.flm.gm_evidence import FLMSample
MAX_WINDOWS = 24
MAX_CONTROL_SAMPLES = 240
MAX_RESPONSE_POINTS = 240
VERSION = 1
LINEAR_POLICY_ID = 'linear_inverse_v1'
# Existing full-rate torqued maturity and vehicle factor envelope, not qlog thresholds.
MIN_TOTAL_POINTS = 4000
MIN_TRAIN_POINTS = 2000
TORQUE_BUCKETS = ((-.5, -.3), (-.3, -.2), (-.2, -.1), (-.1, 0),
(0, .1), (.1, .2), (.2, .3), (.3, .5))
MIN_BUCKET_POINTS = (100, 300, 500, 500, 500, 500, 300, 100)
ENGAGED_NS = 2_000_000_000
# Conservative upper-rounded two-sided 95% Student-t quantiles for df1..23.
# Intervals assume independent, approximately normal block estimates; they are
# statistical model evidence, not physical safety limits or tracking guarantees.
T95 = (12.707, 4.303, 3.183, 2.777, 2.571, 2.447, 2.365, 2.307,
2.263, 2.229, 2.202, 2.179, 2.161, 2.145, 2.132, 2.120,
2.110, 2.102, 2.094, 2.087, 2.080, 2.074, 2.069)
@dataclass(frozen=True)
class PhysicalPoint:
mono_ns: int
applied_torque: float
lateral_accel: float # calibrated v*yaw - g*sin(roll), aligned using frozen lag
@dataclass(frozen=True)
class EvidenceWindow:
identity_sha256: str
block_id: str
controls: tuple[FLMSample, ...] # sampled metrics, never 100 Hz symptom classification
response: tuple[PhysicalPoint, ...]
active_since_ns: int # uninterrupted active/no-override history, before alignment
alignment_lag_ns: int # learned outside this fit and frozen before holdout
@dataclass(frozen=True)
class AcceptancePolicy:
identifier: str = LINEAR_POLICY_ID
max_step_fraction: float = .3
LINEAR_POLICY = AcceptancePolicy()
def linear_policy() -> AcceptancePolicy:
"""Implemented algorithm contract, with callback/controller qualification external."""
return LINEAR_POLICY
@dataclass(frozen=True)
class AutomaticResult:
identity_sha256: str
status: str
reason: str
candidate: tuple[tuple[str, float], ...] = ()
metrics: tuple[tuple[str, float], ...] = ()
acceptance_policy_id: str = ''
family: str = 'linearFactor'
version: int = VERSION
def _finite(value):
return type(value) in (int, float) and math.isfinite(value)
def identity_digest(identity: Mapping) -> str:
if not isinstance(identity, Mapping):
raise ValueError('source_identity')
return hashlib.sha256(json.dumps(dict(identity), sort_keys=True, allow_nan=False,
separators=(',', ':')).encode()).hexdigest()
def admit_linear_factor(value, vehicle_factor, baseline_factor, max_step_fraction):
return bool(all(_finite(x) for x in (value, vehicle_factor, baseline_factor, max_step_fraction)) and
vehicle_factor > 0 and baseline_factor > 0 and 0 < max_step_fraction <= .3 and
.7 * vehicle_factor <= value <= 1.3 * vehicle_factor and
(1 - max_step_fraction) * baseline_factor <= value <= (1 + max_step_fraction) * baseline_factor)
def _window(window, identity):
if (not isinstance(window, EvidenceWindow) or window.identity_sha256 != identity or
type(window.block_id) is not str or not 1 <= len(window.block_id) <= 128 or
not 40 <= len(window.controls) <= MAX_CONTROL_SAMPLES or
not 40 <= len(window.response) <= MAX_RESPONSE_POINTS or
type(window.active_since_ns) is not int or window.active_since_ns <= 0 or
type(window.alignment_lag_ns) is not int or not 0 <= window.alignment_lag_ns <= 500_000_000):
raise ValueError('window_source')
rows = window.controls
route = rows[0].route, rows[0].segment
band = 'fast' if rows[0].v_ego < 25 else 'highway'
for index, row in enumerate(rows):
if (not isinstance(row, FLMSample) or (row.route, row.segment) != route or
not all(_finite(x) for x in (row.t, row.v_ego, row.actual_la, row.desired_la, row.desired_jerk)) or
not row.lat_active or row.steering_pressed or row.saturated or not 15 < row.v_ego <= 90 or
('fast' if row.v_ego < 25 else 'highway') != band):
raise ValueError('window_control')
if index and not 5_000_000 <= round(row.t * 1e9) - round(rows[index - 1].t * 1e9) <= 100_000_000:
raise ValueError('control_cadence_or_gap')
points = []
cursor = 0
direction = None
for point in window.response:
if (not isinstance(point, PhysicalPoint) or type(point.mono_ns) is not int or point.mono_ns <= 0 or
not _finite(point.applied_torque) or not _finite(point.lateral_accel) or
not .02 < abs(point.applied_torque) or not -.5 <= point.applied_torque < .5 or abs(point.lateral_accel) > 1 or
point.mono_ns - window.alignment_lag_ns - window.active_since_ns < ENGAGED_NS or
points and not 0 < point.mono_ns - points[-1].mono_ns <= 100_000_000):
raise ValueError('response_source_or_engagement')
while cursor + 1 < len(rows) and round(rows[cursor + 1].t * 1e9) <= point.mono_ns:
cursor += 1
if (not round(rows[0].t * 1e9) <= point.mono_ns or
not 0 <= point.mono_ns - round(rows[cursor].t * 1e9) <= 30_000_000):
raise ValueError('response_join')
side = 'left' if point.applied_torque > 0 else 'right'
if direction is not None and side != direction:
raise ValueError('mixed_direction')
direction = side
points.append(point)
mean_t = statistics.mean(p.applied_torque for p in points)
mean_a = statistics.mean(p.lateral_accel for p in points)
centered = tuple((p.applied_torque - mean_t, p.lateral_accel - mean_a) for p in points)
energy = sum(t * t for t, _ in centered)
if energy <= 0:
raise ValueError('response_unexcited')
slope = sum(t * a for t, a in centered) / energy
if not _finite(slope) or slope <= 0:
raise ValueError('response_unidentifiable')
return direction, band, centered, slope
def _loss(centered, factor):
return statistics.mean((actual - factor * torque) ** 2 for torque, actual in centered)
def _mean_radius(values):
return T95[len(values) - 2] * statistics.stdev(values) / math.sqrt(len(values))
def _lower_mean(values):
return statistics.mean(values) - _mean_radius(values)
def evaluate_linear_factor(identity: Mapping, windows: tuple[EvidenceWindow, ...], *,
baseline_factor: float, vehicle_factor: float,
linear_conversion_verified: bool,
policy: AcceptancePolicy | None = LINEAR_POLICY) -> AutomaticResult:
source = identity_digest(identity)
def result(status, reason, *, candidate=(), metrics=()):
return AutomaticResult(source, status, reason, candidate, metrics,
policy.identifier if policy is not None else '')
if linear_conversion_verified is not True:
return result('unavailable', 'conversion_not_identified')
if policy != linear_policy():
return result('unavailable', 'algorithm_policy_not_admitted')
assert policy is not None
if not admit_linear_factor(baseline_factor, vehicle_factor, baseline_factor, policy.max_step_fraction):
return result('unavailable', 'baseline_outside_vehicle_bounds')
if (type(windows) is not tuple or len(windows) > MAX_WINDOWS or
any(not isinstance(w, EvidenceWindow) for w in windows) or
len({w.block_id for w in windows}) != len(windows)):
return result('rejected', 'window_limit_or_duplicate')
if len(windows) < 16:
return result('collecting', 'independent_blocks_insufficient')
try:
ranges = {}
for window in windows:
key = window.controls[0].route
start = round(window.controls[0].t * 1e9)
end = max(round(window.controls[-1].t * 1e9), window.response[-1].mono_ns)
if key in ranges and start <= ranges[key]:
raise ValueError('overlapping_or_reordered_windows')
ranges[key] = end
parsed = [_window(window, source) for window in windows]
except (AttributeError, IndexError, OverflowError, TypeError, ValueError, ZeroDivisionError):
return result('rejected', 'invalid_or_unidentifiable_evidence')
if len({w.alignment_lag_ns for w in windows}) != 1:
return result('rejected', 'alignment_changed_across_holdout')
counts = [sum(low <= point.applied_torque < high for w in windows for point in w.response)
for low, high in TORQUE_BUCKETS]
if sum(len(w.response) for w in windows) < MIN_TOTAL_POINTS or any(
count < minimum for count, minimum in zip(counts, MIN_BUCKET_POINTS, strict=True)):
return result('collecting', 'physical_excitation_insufficient')
# Complete chronological blocks are split before fitting; heldout never
# adjusts lag or the candidate factor. Per-block centering is the same fixed
# nuisance-removal objective for baseline and candidate, not an offset update.
split = len(windows) // 2
train, heldout = parsed[:split], parsed[split:]
if sum(len(w.response) for w in windows[:split]) < MIN_TRAIN_POINTS:
return result('collecting', 'training_points_insufficient')
cohorts = tuple((side, band) for side in ('left', 'right') for band in ('fast', 'highway'))
if any(sum(row[:2] == cohort for row in partition) < 2 for partition in (train, heldout) for cohort in cohorts):
return result('collecting', 'independent_direction_speed_support_insufficient')
slopes = [row[3] for row in train]
factor = statistics.mean(slopes)
slope_radius = _mean_radius(slopes)
if any(not admit_linear_factor(value, vehicle_factor, baseline_factor, policy.max_step_fraction)
for value in (factor - slope_radius, factor, factor + slope_radius)):
return result('rejected', 'factor_envelope_or_step')
if factor - slope_radius <= baseline_factor <= factor + slope_radius:
return result('collecting', 'factor_change_not_identified')
base_losses = [_loss(row[2], baseline_factor) for row in heldout]
fit_losses = [_loss(row[2], factor) for row in heldout]
improvement = [before - after for before, after in zip(base_losses, fit_losses, strict=True)]
metrics = (('trainWindows', float(len(train))), ('heldoutWindows', float(len(heldout))),
('trainBaselineMSE', statistics.mean(_loss(row[2], baseline_factor) for row in train)),
('trainFitMSE', statistics.mean(_loss(row[2], factor) for row in train)),
('heldoutBaselineMSE', statistics.mean(base_losses)), ('heldoutFitMSE', statistics.mean(fit_losses)),
('factorEstimate', factor), ('factorBlockIntervalRadius', slope_radius),
('heldoutImprovementLowerBound', _lower_mean(improvement)))
if dict(metrics)['trainFitMSE'] >= dict(metrics)['trainBaselineMSE']:
return result('rejected', 'training_response_fit_insufficient', metrics=metrics)
for cohort in cohorts:
cohort_improvement = [value for row, value in zip(heldout, improvement, strict=True) if row[:2] == cohort]
if _lower_mean(cohort_improvement) < 0:
return result('rejected', 'heldout_direction_speed_regression', metrics=metrics)
if _lower_mean(improvement) <= 0:
return result('rejected', 'heldout_response_fit_insufficient', metrics=metrics)
return result('qualified_fit', 'measured_linear_response_fit', candidate=(('latAccelFactor', factor),), metrics=metrics)