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