Files
StarPilot/openpilot/starpilot/flm/pid_controller_replay.py
2026-10-21 23:28:39 -05:00

132 lines
6.8 KiB
Python

"""Registered torque-actuated PID replay; never reads inactive torque tuning."""
import math
from openpilot.starpilot.flm.physical_response import request_force_accel
from types import SimpleNamespace
PID_SNAPSHOT_REVISION = 'selected-pid-frozen-state-v1'
PID_STAGE_REVISION = 'pid-selected-stage-snapshot-v2'
PID_FIELDS = ('dt', 'sat_limit', 'sat_time', 'sat_check_min_speed', 'steer_max', 'ff_factor', 'automatic_pid_previous_setpoint')
def capture_pid_snapshot(controller, vm):
from openpilot.starpilot.flm.controller_replay import Snapshot, _tree, _object, _VM_FIELDS
values = []
for name in PID_FIELDS:
value = getattr(controller, name)
if value is None:
values.append(('controller.' + name + '.none', 1.))
else:
_tree(values, 'controller.' + name, value)
_object(values, 'pid', controller.pid)
for name in _VM_FIELDS:
_tree(values, 'vm.' + name, getattr(vm, name))
return Snapshot(PID_SNAPSHOT_REVISION, tuple(values))
def pid_snapshot_from_values(values):
from openpilot.starpilot.flm.controller_replay import Snapshot, snapshot_from_values
checked = snapshot_from_values(values)
return Snapshot(PID_SNAPSHOT_REVISION, checked.values)
def restore_pid_snapshot(controller, vm, snapshot):
from openpilot.starpilot.flm.controller_replay import _decode
from openpilot.starpilot.flm.runtime_adapter_registry import baseline_values
if snapshot.revision != PID_SNAPSHOT_REVISION:
raise ValueError('PID checkpoint revision')
original = baseline_values(controller)
def immutable_pid():
scalars = tuple(float(getattr(controller, name)) for name in
('dt', 'sat_limit', 'sat_check_min_speed', 'steer_max', 'ff_factor'))
gains = tuple(tuple(tuple(float(value) for value in row) for row in getattr(controller.pid, key))
for key in ('_k_p', '_k_i', '_k_d'))
return scalars, gains, float(controller.pid.i_dt), float(controller.pid.pos_limit), float(controller.pid.neg_limit)
original_immutable = immutable_pid()
reference = capture_pid_snapshot(controller, vm)
values = dict(snapshot.values)
def shape(rows):
return {key.removesuffix('.none') for key, _ in rows if not any(bit.isdigit() for bit in key.split('.'))}
if shape(reference.values) != shape(snapshot.values):
raise ValueError('incomplete or foreign PID checkpoint')
roots = {'controller': controller, 'pid': controller.pid, 'vm': vm}
top = set()
for key in values:
bits = key.split('.')
index = next((i for i, bit in enumerate(bits) if bit.isdigit() or bit in ('kind', 'length', 'maxlen', 'none')), len(bits))
top.add('.'.join(bits[:index]))
for path in top:
bits = path.split('.')
obj = roots[bits[0]]
for bit in bits[1:-1]:
obj = getattr(obj, bit)
name = bits[-1]
setattr(obj, name, _decode(values, path, getattr(obj, name)))
if baseline_values(controller) != original or immutable_pid() != original_immutable:
raise ValueError('PID checkpoint changed immutable actual gains')
class PidControllerFactory:
def __init__(self, cp, mode, *, turn_assist=False, manual_baseline=(), manual_source_epoch=''):
from opendbc.car import structs
from opendbc.car.car_helpers import interfaces
if (cp.notCar or cp.passive or cp.dashcamOnly or str(cp.carFingerprint) not in interfaces or cp.lateralTuning.which() != 'pid' or
str(cp.steerControlType) != 'torque' or mode != 'native' or turn_assist or manual_baseline or manual_source_epoch):
raise ValueError('registered stock torque PID configuration')
self.cp = structs.CarParams.new_message(**cp.to_dict()).as_reader()
self.mode, self.turn_assist = 'native', False
self.interface_class = interfaces[str(cp.carFingerprint)]
self.manual_baseline, self.manual_source_epoch = (), ''
def create(self):
from opendbc.car.vehicle_model import VehicleModel
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.lateral_controller_factory import create_lateral_controller
ci = self.interface_class(self.cp)
controller = create_lateral_controller(self.cp, ci, .01, mode='native', turn_assist=False)
if type(controller) is not LatControlPID:
raise ValueError('actual stock PID constructor mismatch')
return controller, VehicleModel(self.cp)
def run_pid(controller, vm, inputs, *, effect_trial=None, _prepared_car_state=None):
from openpilot.starpilot.flm.controller_replay import _VM_FIELDS, _replay_car_state
from openpilot.starpilot.flm.selected_effects import EffectReplaySource
values = dict(inputs)
cs = _replay_car_state(inputs, values, _prepared_car_state)
for name in _VM_FIELDS:
setattr(vm, name, values['vm.' + name])
params = SimpleNamespace(angleOffsetDeg=values['angleOffsetDeg'], roll=values['roll'])
source = effect_trial
if source is not None:
if not isinstance(source, EffectReplaySource):
source = EffectReplaySource(source)
source.begin_frame(int(values['control_frame']))
controller.automatic_flm_effects = source
from openpilot.starpilot.flm.pid_stages import PidObservation
observation = PidObservation()
observation.begin_frame()
controller.automatic_flm_observation = observation
output, _, log = controller.update(bool(values['active']), cs, vm, params, bool(values['safety_limited']),
values['desired_curvature'], bool(values['curvature_limited']), values['lat_delay'])
if (source is not None and values['active'] and not cs.steeringPressed and
not values['safety_limited'] and not values['curvature_limited'] and
(source.stage_calls != 1 or source.pid_error_calls != 1 or source.friction_calls != 0 or not controller.automatic_pid_stage_admitted)):
raise ValueError('actual PID effect stage absent/duplicated or fabricated friction')
if not math.isfinite(output):
raise ValueError('PID replay nonfinite output')
return float(output), log
def pid_frame(evidence):
from openpilot.starpilot.flm.controller_replay import ReplayFrame
stage = dict(evidence.stage_values)
return ReplayFrame(evidence.control_ns, evidence.replay_inputs, evidence.requested, evidence.applied_coordinates,
frame_sequence=int(dict(evidence.replay_inputs)['control_frame']), actual_lac_command=stage['selected_command'],
torque_values=(), actual_stage_residual=(0., 0.), centre_values=(0., 0.),
desired_physical_accel=request_force_accel(evidence.replay_inputs),
desired_kinematic_accel=stage['setpoint'],
healthy=evidence.valid and not evidence.fault and not evidence.override, manual=evidence.manual, limited=evidence.limited,
applied_parameter_names=tuple(evidence.applied_parameter_names), effect_trace=evidence.effect_trace,
effect_prepare_ns=evidence.effect_prepare_ns, controller_domain='torque_pid',
vehicle_model_anchor=evidence.vehicle_model_anchor, live_applied_coordinates=evidence.live_applied_coordinates)