mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
a82935f326
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>
238 lines
11 KiB
Python
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)
|