Files
firestar5683 200632aabd StarPilot Auto Learn
Learn bounded feed-forward corrections on eligible torque controllers behind Use StarPilot Auto Learn, preserving installed tunes and native safety limits.
Co-authored-by: Danny <dannyirwin31@yahoo.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: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com>
Co-authored-by: pharmacomaniac <blittle65@gmail.com>

Co-authored-by: whoisdomi <dcantu83@gmail.com>
2026-10-14 20:02:39 -05:00

130 lines
5.3 KiB
Python

# Copyright (c) 2026, firestar5683 and StarPilot contributors
# SPDX-License-Identifier: MIT
"""Selected objective torque bridge; core fits and local replay qualifies each family.
No physical inverse inferred from native controller table; no factor/PID writes.
Two direction cells per speed/phase session fit inside 24 maneuver bound.
"""
from dataclasses import dataclass
import math
from openpilot.starpilot.flm.adaptive_profile import Parameter, ProfileOwner, ReplayValidation
from openpilot.starpilot.flm.torque_stages import REVISION
@dataclass(frozen=True)
class Objective:
stage: str
speed_band: str
phase: str
def __post_init__(self):
if (
self.stage not in ('feedforward', 'friction', 'center')
or self.speed_band not in ('low', 'high')
or self.phase not in ('steady', 'turn_in', 'unwind', 'center')
):
raise ValueError('unknown objective')
if (self.stage == 'center') != (self.phase == 'center'):
raise ValueError('center requires separate objective')
@property
def parameters(self):
low, high, slew = (0.0, 0.01, 0.002) if self.stage == 'center' else (-0.03, 0.03, 0.005)
return tuple(Parameter(f'{self.stage}.{self.speed_band}.{side}.{self.phase}', REVISION, low, high, high, slew) for side in ('left', 'right'))
@property
def required_cohorts(self):
return tuple(f'{self.speed_band}.{side}.{self.phase}' for side in ('left', 'right'))
def gates(objective, speed, setpoint, jerk):
if not all(math.isfinite(v) for v in (speed, setpoint, jerk)) or not 0 <= speed <= 90:
raise ValueError('bad frame')
high = min(max((speed - 10.0) / 15.0, 0.0), 1.0)
speed_gate = high if objective.speed_band == 'high' else 1 - high
if objective.phase == 'center':
phase_gate = max(0.0, 1 - abs(setpoint) / 0.25) * max(0.0, 1 - abs(jerk) / 0.5)
else:
phase = setpoint * jerk
if objective.phase == 'steady':
phase_gate = max(0.0, 1 - abs(phase) / 0.15)
elif objective.phase == 'turn_in':
phase_gate = min(max(phase / 0.15, 0.0), 1.0)
else:
phase_gate = min(max(-phase / 0.15, 0.0), 1.0)
phase_gate *= min(abs(setpoint) / 0.25, 1.0)
return tuple(speed_gate * phase_gate * (1.0 if (setpoint > 0 if side == 'left' else setpoint < 0) else 0.0) for side in ('left', 'right'))
def features(objective, frame, *, center_response_basis=None):
weights = gates(objective, frame.speed, frame.setpoint, frame.jerk)
if objective.stage == 'feedforward':
base = frame.baseline_ff
elif objective.stage == 'friction':
# Actual selected friction derivative belongs to separate physical objective;
# threshold value alone cannot represent command sensitivity.
raise ValueError('use independently measured friction stage sensitivity')
elif center_response_basis is not None and math.isfinite(center_response_basis):
base = center_response_basis
else:
raise ValueError('center physical sensitivity missing')
return tuple(base * w for w in weights)
@dataclass(frozen=True)
class Admission:
profile: object | None
now_ns: int
enabled: bool
manual: bool
healthy: bool
replay_validation: ReplayValidation | None
class TorqueStageOwner:
def __init__(self, identity, objective, *, baseline_revision, physical_model_revision):
if not baseline_revision or not physical_model_revision:
raise ValueError('exact local revisions required')
self.objective = objective
self.owner = ProfileOwner(identity, objective.parameters, baseline_revision=baseline_revision, physical_model_revision=physical_model_revision)
self.baseline_revision, self.physical_model_revision = baseline_revision, physical_model_revision
self.admission = None
self.used = False
self.status = 'collecting'
@property
def applied_coordinates(self):
return self.owner.applied
def prepare(self, admission):
if not isinstance(admission, Admission):
raise ValueError('bad admission')
self.admission, self.used = admission, False
def _tick(self, active):
a = self.admission
v = a.replay_validation
exact = isinstance(v, ReplayValidation) and v.baseline_revision == self.baseline_revision and v.physical_model_revision == self.physical_model_revision
return self.owner.tick(
a.profile, now_ns=a.now_ns, active=active, enabled=a.enabled, manual=a.manual, healthy=a.healthy, replay_validation=v if exact else None
)
def torque_residual(self, frame, *, stage_revision):
if stage_revision != REVISION or self.admission is None or self.used or self.objective.stage == 'center':
return 0.0, 0.0
self.used = True
values = self._tick(frame.active)
delta = sum(a * b for a, b in zip(values, gates(self.objective, frame.speed, frame.setpoint, frame.jerk), strict=True))
self.status = 'applied' if delta else 'collecting'
return (delta, 0.0) if self.objective.stage == 'feedforward' else (0.0, delta)
def torque_deadband(self, speed, *, stage_revision, setpoint=0.0, jerk=0.0):
# Existing hook has only speed; center application must supply actual frame
# inputs through revised center hook before this objective can apply.
if stage_revision != REVISION or self.admission is None or self.used or self.objective.stage != 'center':
return 0.0
self.used = True
values = self._tick(True)
return sum(a * b for a, b in zip(values, gates(self.objective, speed, setpoint, jerk), strict=True))