diff --git a/openpilot/starpilot/flm/clipping_quality.py b/openpilot/starpilot/flm/clipping_quality.py index 6b8c35d7c3..238d1d1f31 100644 --- a/openpilot/starpilot/flm/clipping_quality.py +++ b/openpilot/starpilot/flm/clipping_quality.py @@ -5,35 +5,37 @@ RECORDED_DEBT = 'recorded-debt-v1' STATE_ALIGNED_DEBT = 'candidate-state-debt-v1' STATE_ALIGNED_RELIEF = 'candidate-state-relief-v2' EXACT_EMISSION_QUALITY = 'candidate-exact-emission-v3' +ZERO_REFERENCE_QUALITY = 'candidate-zero-reference-exact-v4' QUALIFIED_EVENT_CLIPPING_QUALITY = EXACT_EMISSION_QUALITY STRICT_IDENTIFICATION = 'strict-final-quality-v1' NATIVE_SHADOW_IDENTIFICATION = 'native-limited-shadow-v1' FIXED_MAP_IDENTIFICATION = 'fixed-map-no-identification-v1' RESTORE_ACTIVATION_VERSION = 'fixed-eight-whole-map-restore-v1' RESTORE_AGGREGATE_ACTIVATION_VERSION = 'fixed-eight-whole-map-restore-support-v2' +RESTORE_ZERO_REFERENCE_VERSION = 'fixed-eight-whole-map-restore-zero-reference-v3' def clipping_quality(value): - if type(value) is not str or value not in (RECORDED_DEBT, STATE_ALIGNED_DEBT, STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY): + if type(value) is not str or value not in (RECORDED_DEBT, STATE_ALIGNED_DEBT, STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY): raise ValueError('unknown clipping quality contract') return value def event_clipping_quality(payload): - if type(payload.get('event_version')) is int and payload['event_version'] in (4, 5): + if type(payload.get('event_version')) is int and payload['event_version'] in (4, 5, 6): if (payload.get('event_operation') != 'restore' or - payload.get('event_restore_policy') != {4: RESTORE_ACTIVATION_VERSION, 5: RESTORE_AGGREGATE_ACTIVATION_VERSION}[payload['event_version']] or - payload.get('event_clipping_quality') != QUALIFIED_EVENT_CLIPPING_QUALITY or + payload.get('event_restore_policy') != restore_policy(payload['event_version']) or + payload.get('event_clipping_quality') != restore_quality(payload['event_version']) or payload.get('event_identification_policy') != FIXED_MAP_IDENTIFICATION): raise ValueError('explicit whole-map restore policy/operation') - return QUALIFIED_EVENT_CLIPPING_QUALITY + return restore_quality(payload['event_version']) if 'event_restore_policy' in payload: - raise ValueError('whole-map restore policy requires version4/5') + raise ValueError('whole-map restore policy requires version4/5/6') if 'event_clipping_quality' in payload and payload.get('event_version') != 3: raise ValueError('explicit clipping quality requires event version3') contract = clipping_quality(payload.get('event_clipping_quality', RECORDED_DEBT)) expected = identification_for_quality(contract) - if contract in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY) and payload.get('event_identification_policy') != expected: + if contract in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY) and payload.get('event_identification_policy') != expected: raise ValueError('combined event experiment requires explicit native shadow identification') if 'event_identification_policy' in payload and (payload.get('event_version') != 3 or identification_policy(payload['event_identification_policy']) != expected): @@ -88,4 +90,36 @@ def identification_policy(value): def identification_for_quality(contract): clipping_quality(contract) - return NATIVE_SHADOW_IDENTIFICATION if contract in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY) else STRICT_IDENTIFICATION + return NATIVE_SHADOW_IDENTIFICATION if contract in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY) else STRICT_IDENTIFICATION + + +def restore_policy(version): + if type(version) is not int or version not in (4, 5, 6): + raise ValueError('explicit whole-map restore version') + return {4: RESTORE_ACTIVATION_VERSION, 5: RESTORE_AGGREGATE_ACTIVATION_VERSION, 6: RESTORE_ZERO_REFERENCE_VERSION}[version] + + +def restore_quality(version): + restore_policy(version) + return ZERO_REFERENCE_QUALITY if version == 6 else QUALIFIED_EVENT_CLIPPING_QUALITY + + +def zero_reference_relief_is_admitted(recorded_request, reference_request, candidate_request, + recorded_output, reference_output, candidate_output, + recorded_debt, reference_debt, candidate_debt): + """Exact paired-output boundary of UNWIND; never native or physical authority. + + Callers must additionally certify original LIMITED context, audited codec, + exact candidate/reference raw integer equality and full native projection. + Honda supplies shaped normalized requests and decoded physical-domain output. + """ + requests = recorded_request, reference_request, candidate_request + outputs = recorded_output, reference_output, candidate_output + values = requests + outputs + (recorded_debt, reference_debt, candidate_debt) + if any(type(value) not in (int, float) or not math.isfinite(value) for value in values): + return False + sign = 1 if recorded_request > 0 else -1 + return bool(reference_debt == 0 and all(sign * value > 0 for value in requests + outputs) and + recorded_debt * recorded_request < 0 and candidate_debt * candidate_request < 0 and + abs(candidate_request) < min(abs(recorded_request), abs(reference_request)) and + candidate_output == reference_output and abs(candidate_output) <= abs(recorded_output)) diff --git a/openpilot/starpilot/flm/controller_replay.py b/openpilot/starpilot/flm/controller_replay.py index 0f9ccfaa13..0389325fec 100644 --- a/openpilot/starpilot/flm/controller_replay.py +++ b/openpilot/starpilot/flm/controller_replay.py @@ -52,6 +52,18 @@ def _numeric(value): def _tree(values, path, value): """Container shape markers prevent stale factory shape from dropping gains.""" + kind = type(value) + if kind is float or kind is int or kind is bool: + # Exact builtin leaves only. Preserve the original coercion, Float64 bits, + # nonfinite error and post-append cardinality guard. Subclasses/NumPy/native + # containers continue through the original generic traversal below. + number = float(value) + if not math.isfinite(number): + raise ValueError('snapshot requires finite numeric state') + values.append((path, number)) + if len(values) > MAX_SNAPSHOT_VALUES: + raise ValueError('snapshot exceeds bounded primitive budget') + return if type(value).__module__.startswith('capnp') and 'List' in type(value).__name__: # Native PID gains contain Cap'n Proto list readers. Only scalar contents # cross the wire; reconstructing a builtin list preserves interpolation. @@ -388,15 +400,57 @@ class SelectedControllerFactory: return controller, VehicleModel(self.cp) -def _run(controller, vm, inputs, objective, coordinates, torque_values=(), centre=None, actual_stage_residual=None, effect_trial=None): - from opendbc.car import structs +@dataclass(frozen=True) +class _PreparedCarState: + inputs: tuple + reader: object + +def _car_state(values, *, first_segment_words=None): + # Keep the original Capnp conversions, including Float32 fields and enum. + from opendbc.car import structs + cs = (structs.CarState.new_message() if first_segment_words is None else + structs.CarState.new_message(num_first_segment_words=first_segment_words)) + for name in _CS_FIELDS: + setattr(cs, name, bool(values[name]) if name in ('steeringPressed', 'standstill', 'steerFaultTemporary', 'steerFaultPermanent') else values[name]) + cs.gearShifter = int(values['gearShifter']) + return cs + + +class _PreparedCarStates: + """One child job only: immutable input tuple refs, never frame-object refs.""" + def __init__(self): + self.__entries = {} + + def get(self, inputs): + saved = self.__entries.get(id(inputs)) + if saved is not None and saved.inputs is inputs: + return saved + if (len(self.__entries) >= 12000 or type(inputs) is not tuple or + any(type(row) is not tuple or len(row) != 2 or type(row[0]) is not str or + type(row[1]) not in (bool, int, float, str) for row in inputs)): + return None + # Strongly retain the deeply immutable tuple: identity cannot be recycled. + prepared = _PreparedCarState(inputs, _car_state(dict(inputs), first_segment_words=32).as_reader()) + self.__entries[id(inputs)] = prepared + return prepared + + +def _replay_car_state(inputs, values, prepared): + if type(prepared) is _PreparedCarState and prepared.inputs is inputs: + return prepared.reader + return _car_state(values) + + +def _run(controller, vm, inputs, objective, coordinates, torque_values=(), centre=None, actual_stage_residual=None, effect_trial=None, + _prepared_car_state=None): from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID if type(controller) is LatControlPID: from openpilot.starpilot.flm.pid_controller_replay import run_pid if torque_values or any(coordinates) or centre is not None and any(centre.values): raise ValueError('PID cannot consume torque tuning or legacy residual coordinates') - return run_pid(controller, vm, inputs, effect_trial=effect_trial) + kwargs = {} if _prepared_car_state is None else {'_prepared_car_state': _prepared_car_state} + return run_pid(controller, vm, inputs, effect_trial=effect_trial, **kwargs) values = dict(inputs) if torque_values: tune = dict(torque_values) @@ -410,10 +464,7 @@ def _run(controller, vm, inputs, objective, coordinates, torque_values=(), centr controller.torque_params.latAccelOffset = tune['offset'] controller.torque_params.friction = tune['friction'] controller.pid.set_limits(tune['pid_pos_limit'], tune['pid_neg_limit']) - cs = structs.CarState.new_message() - for name in _CS_FIELDS: - setattr(cs, name, bool(values[name]) if name in ('steeringPressed', 'standstill', 'steerFaultTemporary', 'steerFaultPermanent') else values[name]) - cs.gearShifter = int(values['gearShifter']) + 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']) @@ -588,6 +639,40 @@ class ControllerReplay: return bool(frame.healthy and not frame.manual and (not frame.limited or self.raw_mode and replay_context(frame))) + def _recorded_cp_digest(self): + """One successful canonical CP digest; reread ALL native bytes each lookup. + + Only owned registered CarParams readers share this replay-local derived + value. No object identity shortcut, payload flag, global/current cache or + cached error. Unknown schemas/objects keep the original canonical function. + """ + from capnp.lib.capnp import _DynamicStructReader + from opendbc.car import structs + from openpilot.starpilot.flm.pid_controller_replay import PidControllerFactory + from openpilot.starpilot.flm.runtime_adapter_registry import car_params_digest + + cp = self.factory.cp + raw = None + try: + if ((type(self.factory) is SelectedControllerFactory or type(self.factory) is PidControllerFactory) and + type(cp) is _DynamicStructReader and cp.schema == structs.CarParams.schema): + raw = cp.as_builder().to_bytes() + except Exception: + pass # Original digest below retains its own errors and unsupported behavior. + if type(raw) is not bytes: + return car_params_digest(cp) + saved = self.__dict__.get('_ControllerReplay__recorded_cp_digest_cache') + if type(saved) is tuple and len(saved) == 3 and saved[0] == raw and saved[2] is car_params_digest: + return saved[1] + value = car_params_digest(cp) + try: + unchanged = cp.as_builder().to_bytes() == raw + except Exception: + unchanged = False + if unchanged and type(value) is str and len(value) == 64 and all(char in '0123456789abcdef' for char in value): + self.__dict__['_ControllerReplay__recorded_cp_digest_cache'] = (raw, value, car_params_digest) + return value + def normalize(self, frame, *, epsilon=0.0001, _verify_only=False): if ( self.failed @@ -615,10 +700,9 @@ class ControllerReplay: recorded_effect = None if frame.effect_trace is not None: from openpilot.starpilot.flm.selected_effects import EffectReplayRecordedSource - from openpilot.starpilot.flm.runtime_adapter_registry import car_params_digest if (frame.effect_trace.control_ns != frame.effect_prepare_ns or frame.effect_trace.envelope.selected_baseline_revision != self.baseline_revision or - frame.effect_trace.envelope.cp_sha256 != car_params_digest(self.factory.cp)): + frame.effect_trace.envelope.cp_sha256 != self._recorded_cp_digest()): raise ValueError('recorded full30 trace source/prepare clock mismatch') recorded_effect = EffectReplayRecordedSource(frame.effect_trace) _verify_only = True diff --git a/openpilot/starpilot/flm/event_activation.py b/openpilot/starpilot/flm/event_activation.py index b02c117808..f99717c41a 100644 --- a/openpilot/starpilot/flm/event_activation.py +++ b/openpilot/starpilot/flm/event_activation.py @@ -6,15 +6,18 @@ import math from openpilot.starpilot.flm.physical_response import PhysicalModel from openpilot.starpilot.flm.clipping_quality import ( - RECORDED_DEBT, STATE_ALIGNED_DEBT, STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, - RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION, QUALIFIED_EVENT_CLIPPING_QUALITY, clipping_quality, identification_for_quality, + RECORDED_DEBT, STATE_ALIGNED_DEBT, STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY, + RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION, RESTORE_ZERO_REFERENCE_VERSION, + QUALIFIED_EVENT_CLIPPING_QUALITY, clipping_quality, identification_for_quality, ) ACTIVATION_VERSION = 'fixed-eight-owner-v1' STATE_ALIGNED_ACTIVATION_VERSION = 'fixed-eight-owner-state-aligned-v2' COMBINED_ACTIVATION_VERSION = 'fixed-eight-shadow-relief-v3' EXACT_EMISSION_ACTIVATION_VERSION = 'fixed-eight-shadow-exact-emission-v4' -ACTIVATION_VERSIONS = (ACTIVATION_VERSION, STATE_ALIGNED_ACTIVATION_VERSION, COMBINED_ACTIVATION_VERSION, EXACT_EMISSION_ACTIVATION_VERSION) +ZERO_REFERENCE_ACTIVATION_VERSION = 'fixed-eight-shadow-native-feasible-v6' +ACTIVATION_VERSIONS = (ACTIVATION_VERSION, STATE_ALIGNED_ACTIVATION_VERSION, COMBINED_ACTIVATION_VERSION, + EXACT_EMISSION_ACTIVATION_VERSION, ZERO_REFERENCE_ACTIVATION_VERSION) CALIBRATION_EVENTS = 8 @@ -76,7 +79,8 @@ class EventActivationPlan: def clipping_quality(self): self.validate() return {ACTIVATION_VERSION: RECORDED_DEBT, STATE_ALIGNED_ACTIVATION_VERSION: STATE_ALIGNED_DEBT, - COMBINED_ACTIVATION_VERSION: STATE_ALIGNED_RELIEF, EXACT_EMISSION_ACTIVATION_VERSION: EXACT_EMISSION_QUALITY}[self.version] + COMBINED_ACTIVATION_VERSION: STATE_ALIGNED_RELIEF, EXACT_EMISSION_ACTIVATION_VERSION: EXACT_EMISSION_QUALITY, + ZERO_REFERENCE_ACTIVATION_VERSION: ZERO_REFERENCE_QUALITY}[self.version] @property def identification_policy(self): @@ -100,7 +104,8 @@ def build_activation_plan(engine, *, model, cutoff_ns, stage_events, physical_co """No trial coordinates, rollout results, desired gains or cutoff search here.""" clipping_quality(quality_contract) version = {RECORDED_DEBT: ACTIVATION_VERSION, STATE_ALIGNED_DEBT: STATE_ALIGNED_ACTIVATION_VERSION, - STATE_ALIGNED_RELIEF: COMBINED_ACTIVATION_VERSION, EXACT_EMISSION_QUALITY: EXACT_EMISSION_ACTIVATION_VERSION}[quality_contract] + STATE_ALIGNED_RELIEF: COMBINED_ACTIVATION_VERSION, EXACT_EMISSION_QUALITY: EXACT_EMISSION_ACTIVATION_VERSION, + ZERO_REFERENCE_QUALITY: ZERO_REFERENCE_ACTIVATION_VERSION}[quality_contract] frames = {frame.control_ns: frame for frame in engine.records} windows = tuple(engine.replay_windows) if (not stage_events or any(min(point['control_ns'], point['apply_ns'], point['mono_ns']) <= cutoff_ns @@ -154,7 +159,8 @@ def activation_model_from_dict(value): if 'activation_version' not in copied: return PhysicalModel(**copied) model = ActivatedPhysicalModel(**copied) - if (model.activation_version not in (*ACTIVATION_VERSIONS, RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION) or + restore_versions = (RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION, RESTORE_ZERO_REFERENCE_VERSION) + if (model.activation_version not in (*ACTIVATION_VERSIONS, *restore_versions) or not _hex64(model.activation_plan_sha256) or model.domain != 'normalized_torque' or any(type(value) not in (int, float) or not math.isfinite(value) for value in @@ -174,10 +180,13 @@ def activation_matches_model(plan, model): def model_clipping_quality(model): if type(model) is ActivatedPhysicalModel: + if model.activation_version == RESTORE_ZERO_REFERENCE_VERSION: + return ZERO_REFERENCE_QUALITY if model.activation_version in (RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION): return QUALIFIED_EVENT_CLIPPING_QUALITY if model.activation_version not in ACTIVATION_VERSIONS: raise ValueError('unknown activated physical proof version') return {ACTIVATION_VERSION: RECORDED_DEBT, STATE_ALIGNED_ACTIVATION_VERSION: STATE_ALIGNED_DEBT, - COMBINED_ACTIVATION_VERSION: STATE_ALIGNED_RELIEF, EXACT_EMISSION_ACTIVATION_VERSION: EXACT_EMISSION_QUALITY}[model.activation_version] + COMBINED_ACTIVATION_VERSION: STATE_ALIGNED_RELIEF, EXACT_EMISSION_ACTIVATION_VERSION: EXACT_EMISSION_QUALITY, + ZERO_REFERENCE_ACTIVATION_VERSION: ZERO_REFERENCE_QUALITY}[model.activation_version] return RECORDED_DEBT diff --git a/openpilot/starpilot/flm/event_collection.py b/openpilot/starpilot/flm/event_collection.py index 4adf219240..6d367a1b75 100644 --- a/openpilot/starpilot/flm/event_collection.py +++ b/openpilot/starpilot/flm/event_collection.py @@ -47,9 +47,9 @@ class EventCollectionAdapter(TorqueCollectionAdapter): self.clipping_quality = validate_clipping_quality(clipping_quality) if replay_version != 3 and self.clipping_quality != RECORDED_DEBT: raise ValueError('state-aligned quality requires explicit event version3') - from openpilot.starpilot.flm.clipping_quality import QUALIFIED_EVENT_CLIPPING_QUALITY - if type(restore_version) is not int or restore_version not in (2, 4, 5) or (restore_version in (4, 5) and - (replay_version != 3 or self.clipping_quality != QUALIFIED_EVENT_CLIPPING_QUALITY)): + from openpilot.starpilot.flm.clipping_quality import restore_quality + if type(restore_version) is not int or restore_version not in (2, 4, 5, 6) or (restore_version in (4, 5, 6) and + (replay_version != 3 or self.clipping_quality != restore_quality(restore_version))): raise ValueError('explicit whole-map restore producer contract') self.restore_version = restore_version self.replay_version = replay_version @@ -212,14 +212,14 @@ class EventCollectionAdapter(TorqueCollectionAdapter): envelope = self.envelope.for_trial(self.event_objective.names).restore_from_history(self.optimizer_prior) envelope.validate() operation = 'restore' - from openpilot.starpilot.flm.clipping_quality import FIXED_MAP_IDENTIFICATION, RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION + from openpilot.starpilot.flm.clipping_quality import FIXED_MAP_IDENTIFICATION, restore_policy payload = {'version': 1, 'family': self.command_family, 'event_version': self.replay_version if operation == 'trial' else self.restore_version, 'event_operation': operation, **({'event_clipping_quality': self.clipping_quality, 'event_identification_policy': identification_for_quality(self.clipping_quality)} if self.replay_version == 3 and operation == 'trial' else {}), **({'event_clipping_quality': self.clipping_quality, 'event_identification_policy': FIXED_MAP_IDENTIFICATION, - 'event_restore_policy': {4: RESTORE_ACTIVATION_VERSION, 5: RESTORE_AGGREGATE_ACTIVATION_VERSION}[self.restore_version]} - if operation == 'restore' and self.restore_version in (4, 5) else {}), + 'event_restore_policy': restore_policy(self.restore_version)} + if operation == 'restore' and self.restore_version in (4, 5, 6) else {}), 'event_objective': asdict(self.event_objective), 'effect_envelope': asdict(envelope), 'objective': asdict(self.objective), 'now_ns': now_ns, 'sequence': sequence, 'prior': tuple(dict(envelope.prior_coordinates)[name] for name in self.event_objective.names), @@ -265,12 +265,13 @@ class EventCollectionAdapter(TorqueCollectionAdapter): not set(result.profile.train_ids + result.profile.heldout_ids).issubset(plan.stage_blocks) or not activation_matches_model(plan, model)): return False - elif report_version in (4, 5): + elif report_version in (4, 5, 6): from openpilot.starpilot.flm.restore_activation import restore_plan_from_dict, restore_plan_matches - from openpilot.starpilot.flm.clipping_quality import RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION + from openpilot.starpilot.flm.clipping_quality import restore_policy plan = restore_plan_from_dict(report['restore_activation_plan']) - expected_policy = {4: RESTORE_ACTIVATION_VERSION, 5: RESTORE_AGGREGATE_ACTIVATION_VERSION}[report_version] + expected_policy = restore_policy(report_version) if (self.restore_version != report_version or plan.version != expected_policy or + report_version == 6 and plan.clipping_quality != self.clipping_quality or envelope.operation != 'restore' or 'activation_plan' in report or plan.identity != self.identity or plan.baseline_revision != self.baseline_revision or plan.source_epoch != self.source_epoch or plan.cutoff_ns != report['model_cutoff_ns'] or @@ -289,7 +290,7 @@ class EventCollectionAdapter(TorqueCollectionAdapter): physical_model_revision(model) != validation.physical_model_revision or validation.baseline_revision != self.baseline_revision): return False self.event_objective = EffectObjective(self.key[1], envelope.trial_names, envelope.trial_anchor, self.source_epoch) - if report_version in (3, 4, 5) and self.model != model: + if report_version in (3, 4, 5, 6) and self.model != model: self.current_physical_valid = False self.last_physical_ns = 0 self.model, self.model_cutoff_ns = model, report['model_cutoff_ns'] diff --git a/openpilot/starpilot/flm/event_controller_replay.py b/openpilot/starpilot/flm/event_controller_replay.py index 302369f027..de209b64f8 100644 --- a/openpilot/starpilot/flm/event_controller_replay.py +++ b/openpilot/starpilot/flm/event_controller_replay.py @@ -1,10 +1,13 @@ """Child-only canonical effect trials through complete selected controller/CAN history.""" +from collections.abc import Mapping import math from openpilot.starpilot.flm.clipping_quality import NATIVE_SHADOW_IDENTIFICATION from openpilot.starpilot.flm.adaptive_profile import ReplayValidation -from openpilot.starpilot.flm.controller_replay import MAX_FRAME_GAP_NS, _run, final_command, restore_snapshot, same_final_command, same_float +from openpilot.starpilot.flm.controller_replay import ( + MAX_FRAME_GAP_NS, _run, capture_snapshot, final_command, restore_snapshot, same_final_command, same_float, +) from openpilot.starpilot.flm.runtime_adapter_registry import canonical_physical_context, physical_model_revision from openpilot.starpilot.flm.torque_eligibility import exact_unclipped_pair @@ -12,6 +15,48 @@ POLICY = 'event_effect_coordinate_v2' RESTORE_POLICY = 'event_effect_restore_v1' +class _LazyControllerRequests(Mapping): + """Only original native replay may pull this child-local controller iterator. + + Values are cached by their exact control timestamp. Native apply-order stays + owned by EmissionHistory.rollout, including duplicate/older requested stamps. + No result is successful until finish() exhausts all original controller guards. + """ + def __init__(self, values, frames): + self.values, self.frames = values, frames + self.finished = False + + def __contains__(self, stamp): + while stamp not in self.values and not self.finished: + try: + next(self.frames) + except StopIteration: + self.finished = True + return stamp in self.values + + def __getitem__(self, stamp): + if stamp not in self: + raise KeyError(stamp) + return self.values[stamp] + + def __iter__(self): + self.finish() + return iter(self.values) + + def __len__(self): + self.finish() + return len(self.values) + + def close(self): + self.frames.close() + + def finish(self): + if not self.finished: + for _ in self.frames: + pass + self.finished = True + + class EventControllerReplay: """Reuse original source/native/encoder proofs; replace only trial coordinates.""" def __init__(self, engine, objective, original_anchor, *, restore_envelope=None, activation_plan=None): @@ -22,6 +67,11 @@ class EventControllerReplay: self.restore_envelope = restore_envelope self.activation_plan = activation_plan self.__identification_token = object() + self.__strict_rollouts = None + self.__causal_features = None + self.__prepared_car_states = None + self.__controller_prefix = None + self.__pending_controller_prefix = None if activation_plan is not None: from openpilot.starpilot.flm.restore_activation import WholeMapRestorePlan, restore_plan_matches activation_plan.validate() @@ -59,6 +109,77 @@ class EventControllerReplay: return False def _rollout(self, coordinates, *, restore_target=False, restore_omit_group=None, _identification_token=None): + """At most baseline+last successful strict trial result, never authority. + + Every hit rebinds exact owned input bytes. No errors, shadow probes, restore + paths or current admissions are memoized. Final validate still checks its + current source/model/sample/support/heldout predicates independently. + """ + from openpilot.starpilot.flm.event_activation import EventActivationPlan, ZERO_REFERENCE_ACTIVATION_VERSION + import pickle + from types import MappingProxyType + + plan = self.__dict__.get('activation_plan') + eligible = (type(plan) is EventActivationPlan and plan.version == ZERO_REFERENCE_ACTIVATION_VERSION and + self.restore_envelope is None and restore_target is False and restore_omit_group is None and _identification_token is None and + type(coordinates) is tuple and all(type(value) in (int, float) and math.isfinite(value) for value in coordinates)) + if not eligible: + # The controller's ORIGINAL prefix is independent of native strict/shadow + # policy. Never reuse native output or a native-policy verdict here. + prefix_key = None + if (_identification_token is not None and + _identification_token is self.__dict__.get('_EventControllerReplay__identification_token') and + self.__dict__.get('_EventControllerReplay__controller_prefix') is not None and + type(plan) is EventActivationPlan and plan.version == ZERO_REFERENCE_ACTIVATION_VERSION and + self.restore_envelope is None and restore_target is False and restore_omit_group is None): + prefix_key = self._causal_input_key(.01) + if prefix_key is None: + return self._rollout_uncached(coordinates, restore_target=restore_target, + restore_omit_group=restore_omit_group, _identification_token=_identification_token) + result = self._rollout_uncached(coordinates, restore_target=restore_target, + restore_omit_group=restore_omit_group, _identification_token=_identification_token, + _prefix_context=(prefix_key, False)) + if self._causal_input_key(.01) != prefix_key: + self.__controller_prefix = None + raise ValueError('controller prefix changed owned input during execution') + return result + source_key = self._causal_input_key(.01) + if source_key is None: + self.__strict_rollouts = None + self.__controller_prefix = None + return self._rollout_uncached(coordinates) + if self.__strict_rollouts is None or self.__strict_rollouts[0] != source_key: + self.__strict_rollouts = source_key, {} + slots = self.__strict_rollouts[1] + coordinate_key = pickle.dumps(coordinates, protocol=5) # Type/bit exact, including signed zero. + slot = 'baseline' if coordinate_key == pickle.dumps(self.reference, protocol=5) else 'candidate' + key = plan.version, 'strict-trial', coordinate_key + cached = slots.get(slot) + if cached is not None and cached[0] == key: + return tuple(dict(part) for part in cached[1]) + from openpilot.starpilot.flm.controller_replay import ControllerReplay + from openpilot.starpilot.flm.torque_emission_replay import EmissionHistory + prefix_scoped = (type(self) is EventControllerReplay and type(self.engine) is ControllerReplay and + type(self.emissions) is EmissionHistory) + build_prefix = slot == 'baseline' and prefix_scoped + self.__pending_controller_prefix = None + if prefix_scoped: + result = self._rollout_uncached(coordinates, _prefix_context=(source_key, build_prefix)) + else: + result = self._rollout_uncached(coordinates) + if self._causal_input_key(.01) != source_key: + self.__strict_rollouts = None + self.__controller_prefix = self.__pending_controller_prefix = None + raise ValueError('strict rollout changed owned input during execution') + if build_prefix: + self.__controller_prefix = self.__pending_controller_prefix + self.__pending_controller_prefix = None + frozen = tuple(MappingProxyType(dict(part)) for part in result) + slots[slot] = key, frozen + return tuple(dict(part) for part in frozen) + + def _rollout_uncached(self, coordinates, *, restore_target=False, restore_omit_group=None, _identification_token=None, + _prefix_context=None): from openpilot.starpilot.flm.selected_effects import (EffectTrial, EffectReplayTransition, EffectReplayRecordedSource, EffectRestoreTrial, EffectRestoreAblationTrial) @@ -98,79 +219,324 @@ class EventControllerReplay: if any(a > b or i and a <= windows[i - 1][1] for i, (a, b) in enumerate(windows)): raise ValueError('event windows overlap') controller, vm = self.factory.create() + # Parent/default/current replay never allocates this child-only reader cache. + from openpilot.starpilot.flm.controller_replay import SelectedControllerFactory, _PreparedCarStates + from openpilot.starpilot.flm.pid_controller_replay import PidControllerFactory + from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque + from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID + supported = (len(frames) <= 12000 and type(self.factory) in (SelectedControllerFactory, PidControllerFactory) and + type(controller) in (LatControlTorque, LatControlPID)) + prepared_states = self.__dict__.get('_EventControllerReplay__prepared_car_states') if supported else None + if supported and type(prepared_states) is not _PreparedCarStates: + prepared_states = self.__prepared_car_states = _PreparedCarStates() transition = None if trial is None or restore_target else EffectReplayTransition(trial) checkpoints, requests, saturated = dict(self.checkpoints), {}, {} - prior_ns = prior_sequence = index = 0 - neutral = not restore_target and (self.restore_envelope is not None or tuple(coordinates) == self.reference) - for frame in frames: - while index < len(windows) and frame.control_ns > windows[index][1]: - index += 1 - if index >= len(windows) or not windows[index][0] <= frame.control_ns <= windows[index][1]: - raise ValueError('event frame outside original window') - if frame.control_ns == windows[index][0]: - if frame.control_ns not in checkpoints: - raise ValueError('event actual pre-maneuver state missing') - restore_snapshot(controller, vm, checkpoints[frame.control_ns]) - if transition is not None: - transition.reset() - prior_ns = prior_sequence = 0 - elif not prior_ns: - raise ValueError('event window beginning omitted') - if (not self.engine._frame_context(frame) or not self._prior_matches(frame) or any(frame.centre_values) or - frame.actual_stage_residual is not None and any(frame.actual_stage_residual) or - prior_ns and not 0 < frame.control_ns - prior_ns <= MAX_FRAME_GAP_NS or - prior_sequence and frame.frame_sequence != prior_sequence + 1): - raise ValueError('event source/history mismatch') - trace = getattr(frame, 'effect_trace', None) - prepare_ns = getattr(frame, 'effect_prepare_ns', 0) - activation = None if self.activation_plan is None else self.activation_plan.activation_for(*windows[index]) - if self.activation_plan is not None and activation is None: - raise ValueError('event activation window omitted') - original_prefix = bool(activation is not None and (activation[0] == 0 or frame.control_ns < activation[0])) - activation_tick = bool(activation is not None and frame.control_ns == activation[0]) - if activation_tick and prepare_ns != activation[1]: - raise ValueError('event activation original prepare clock changed') - if self.activation_plan is not None and restore_target and not original_prefix and not 0 < prepare_ns <= frame.control_ns: - raise ValueError('restore activation original owner clock unavailable') - if neutral or original_prefix: - source = None if trace is None else EffectReplayRecordedSource(trace) - elif restore_target: - # Whole-map endpoint qualification is independent from the separately - # replayed/probed gradual owner transition. Never reduce this map to the - # protocol's <=4 marker coordinates or infer neutral from zero markers. - source = trial - elif prepare_ns > 0: - source = transition.source_for_frame(prepare_ns, trace, new_owner=activation_tick) - elif self.activation_plan is not None: - raise ValueError('event activation post-treatment owner clock missing') - elif trace is None and not any(self.prior_full.values()): - # Historical zero-effect data may qualify a stationary shape proposal; - # it contains no new owner prepare stamp and confers no slew admission. - # Parent current_effect_probe separately requires fresh exact prepare_ns. - source = trial - else: - raise ValueError('event exact owner transition clock unavailable') - output, log = _run(controller, vm, frame.inputs, self.engine.objective, (0.0, 0.0), frame.torque_values, - actual_stage_residual=frame.actual_stage_residual, effect_trial=source) - if (neutral or original_prefix) and (not same_final_command(output, frame.selected_command) or - frame.actual_lac_command is not None and not same_float(output, frame.actual_lac_command)): - raise ValueError('event neutral selected trajectory changed') - output = final_command(output) - if not math.isfinite(output) or abs(output) > 1.000001: - raise ValueError('event selected command envelope') - requests[frame.control_ns], saturated[frame.control_ns] = output, bool(log.saturated) - prior_ns, prior_sequence = frame.control_ns, frame.frame_sequence - if index != len(windows) - 1 or prior_ns != windows[-1][1]: - raise ValueError('event last actual tick missing') - # This replays every consumed native encoding, including clipped unscored - # context. The encoder independently refuses greater/opposite clipping. - quality = {} if self.activation_plan is None else {'quality_contract': self.activation_plan.clipping_quality} - if shadow: - quality['identification_policy'] = NATIVE_SHADOW_IDENTIFICATION - encoded = self.emissions.rollout(requests, windows[0][0], windows[-1][1], windows, **quality) - return requests, encoded, saturated + # Only the wrapper supplies a freshly hashed owned-input context. A new + # certificate is published there AFTER full strict success and rehashing. + from openpilot.starpilot.flm.event_activation import EventActivationPlan, ZERO_REFERENCE_ACTIVATION_VERSION + from types import MappingProxyType + from openpilot.starpilot.flm.torque_emission_replay import EmissionHistory + from openpilot.starpilot.lateral.torque_extension import TorqueExtension + from openpilot.starpilot.lateral.ioniq6_policy import Ioniq6TorquePolicy + extension = getattr(controller, 'starpilot_extension', None) + prefix_supported = (type(controller) is LatControlPID or type(controller) is LatControlTorque and + (extension is None or type(extension) is TorqueExtension and type(extension.policy) is Ioniq6TorquePolicy)) + prefix_eligible = (supported and prefix_supported and type(self.emissions) is EmissionHistory and + type(self.activation_plan) is EventActivationPlan and + self.activation_plan.version == ZERO_REFERENCE_ACTIVATION_VERSION and self.restore_envelope is None and + not restore_target and restore_omit_group is None and _prefix_context is not None) + build_prefix = bool(prefix_eligible and _prefix_context[1] and not shadow and tuple(coordinates) == self.reference) + certificate = self.__dict__.get('_EventControllerReplay__controller_prefix') if prefix_eligible and not build_prefix else None + if certificate is not None and certificate[0] != _prefix_context[0]: + certificate = None + prefix_requests, prefix_saturated, prefix_states = {}, {}, {} + + def controller_frames(): + nonlocal build_prefix + prior_ns = prior_sequence = index = 0 + neutral = not restore_target and (self.restore_envelope is not None or tuple(coordinates) == self.reference) + for frame in frames: + while index < len(windows) and frame.control_ns > windows[index][1]: + index += 1 + if index >= len(windows) or not windows[index][0] <= frame.control_ns <= windows[index][1]: + raise ValueError('event frame outside original window') + if frame.control_ns == windows[index][0]: + if frame.control_ns not in checkpoints: + raise ValueError('event actual pre-maneuver state missing') + restore_snapshot(controller, vm, checkpoints[frame.control_ns]) + if transition is not None: + transition.reset() + prior_ns = prior_sequence = 0 + elif not prior_ns: + raise ValueError('event window beginning omitted') + if (not self.engine._frame_context(frame) or not self._prior_matches(frame) or any(frame.centre_values) or + frame.actual_stage_residual is not None and any(frame.actual_stage_residual) or + prior_ns and not 0 < frame.control_ns - prior_ns <= MAX_FRAME_GAP_NS or + prior_sequence and frame.frame_sequence != prior_sequence + 1): + raise ValueError('event source/history mismatch') + trace = getattr(frame, 'effect_trace', None) + prepare_ns = getattr(frame, 'effect_prepare_ns', 0) + activation = None if self.activation_plan is None else self.activation_plan.activation_for(*windows[index]) + if self.activation_plan is not None and activation is None: + raise ValueError('event activation window omitted') + original_prefix = bool(activation is not None and (activation[0] == 0 or frame.control_ns < activation[0])) + activation_tick = bool(activation is not None and frame.control_ns == activation[0]) + if activation_tick and prepare_ns != activation[1]: + raise ValueError('event activation original prepare clock changed') + reusable_prefix = bool(original_prefix and activation[0] > windows[index][0]) + if activation_tick: + if build_prefix and activation[0] > windows[index][0]: + try: + prefix_states[windows[index]] = capture_snapshot(controller, vm) + except (AttributeError, TypeError, ValueError, KeyError, OverflowError): + # An unavailable optimization checkpoint must NOT reject an + # otherwise ordinary replay. Never publish a partial certificate. + build_prefix = False + prefix_requests.clear() + prefix_saturated.clear() + prefix_states.clear() + elif certificate is not None and windows[index] in certificate[3]: + restore_snapshot(controller, vm, certificate[3][windows[index]]) + if self.activation_plan is not None and restore_target and not original_prefix and not 0 < prepare_ns <= frame.control_ns: + raise ValueError('restore activation original owner clock unavailable') + if neutral or original_prefix: + source = None if trace is None else EffectReplayRecordedSource(trace) + elif restore_target: + # Whole-map endpoint qualification is independent from the separately + # replayed/probed gradual owner transition. Never reduce this map to the + # protocol's <=4 marker coordinates or infer neutral from zero markers. + source = trial + elif prepare_ns > 0: + source = transition.source_for_frame(prepare_ns, trace, new_owner=activation_tick) + elif self.activation_plan is not None: + raise ValueError('event activation post-treatment owner clock missing') + elif trace is None and not any(self.prior_full.values()): + # Historical zero-effect data may qualify a stationary shape proposal; + # it contains no new owner prepare stamp and confers no slew admission. + # Parent current_effect_probe separately requires fresh exact prepare_ns. + source = trial + else: + raise ValueError('event exact owner transition clock unavailable') + if certificate is not None and reusable_prefix and windows[index] in certificate[3]: + # Retain every source/history/clock/prepare/recorded-source check above. + # Later state is restored ONLY at the exact declared activation tick. + output = certificate[1][frame.control_ns] + is_saturated = certificate[2][frame.control_ns] + else: + prepared = None if prepared_states is None else prepared_states.get(frame.inputs) + kwargs = {} if prepared is None else {'_prepared_car_state': prepared} + output, log = _run(controller, vm, frame.inputs, self.engine.objective, (0.0, 0.0), frame.torque_values, + actual_stage_residual=frame.actual_stage_residual, effect_trial=source, **kwargs) + is_saturated = bool(log.saturated) + if (neutral or original_prefix) and (not same_final_command(output, frame.selected_command) or + frame.actual_lac_command is not None and not same_float(output, frame.actual_lac_command)): + raise ValueError('event neutral selected trajectory changed') + raw_output = output # Preserve raw Float64 for the original actual_lac guard. + output = final_command(output) + if not math.isfinite(output) or abs(output) > 1.000001: + raise ValueError('event selected command envelope') + requests[frame.control_ns], saturated[frame.control_ns] = output, is_saturated + if build_prefix and reusable_prefix: + prefix_requests[frame.control_ns], prefix_saturated[frame.control_ns] = raw_output, is_saturated + prior_ns, prior_sequence = frame.control_ns, frame.frame_sequence + yield frame.control_ns + if index != len(windows) - 1 or prior_ns != windows[-1][1]: + raise ValueError('event last actual tick missing') + + lazy_requests = _LazyControllerRequests(requests, controller_frames()) + try: + from openpilot.starpilot.flm.torque_emission_replay import EmissionHistory + native_requests = lazy_requests + if not supported or type(self.emissions) is not EmissionHistory: + lazy_requests.finish() # Unknown/legacy shells retain original eager order. + native_requests = requests # Preserve their original plain-dict API too. + # Original native replay owns every emission, including unscored context. + quality = {} if self.activation_plan is None else {'quality_contract': self.activation_plan.clipping_quality} + if shadow: + quality['identification_policy'] = NATIVE_SHADOW_IDENTIFICATION + encoded = self.emissions.rollout(native_requests, windows[0][0], windows[-1][1], windows, **quality) + # Native success is insufficient: late controller-only frames and the exact + # original final-window/tick guard must ALSO finish before any result exists. + lazy_requests.finish() + if build_prefix and prefix_requests and prefix_states: + self.__pending_controller_prefix = (_prefix_context[0], MappingProxyType(prefix_requests), + MappingProxyType(prefix_saturated), MappingProxyType(prefix_states)) + return requests, encoded, saturated + finally: + # Release suspended controller/native traceback resources on rejection. + # close() NEVER substitutes for the required successful finish() above. + lazy_requests.close() + + def _causal_input_key(self, epsilon): + """Exact owned input bytes, not equality of floats or a caller-ready flag. + + Includes every original frame/native witness/checkpoint, full-map anchor, + selected factory configuration, objective, epsilon and declared plan. + Pickle is only serialized+hashed here; no cache bytes are ever unpickled. + """ + import hashlib + import pickle + + if 'engine' not in self.__dict__: + return None # A legacy/test shell has no owned input history to cache. + try: + from openpilot.starpilot.flm.controller_replay import SelectedControllerFactory + from openpilot.starpilot.flm.pid_controller_replay import PidControllerFactory + factory = self.factory + if type(factory) not in (SelectedControllerFactory, PidControllerFactory): + return None + policy = ('torque', str(factory.policy)) if type(factory) is SelectedControllerFactory else ('stock-pid', None) + selected = (type(factory).__module__, type(factory).__qualname__, + factory.cp.as_builder().to_bytes(), str(factory.mode), policy, factory.turn_assist, + factory.manual_baseline, factory.manual_source_epoch, + factory.interface_class.__module__, factory.interface_class.__qualname__) + native = (self.emissions.cp.as_builder().to_bytes(), self.emissions.identity, self.emissions.baseline_revision, + tuple(self.emissions.events.items()), tuple(self.emissions.carriers.items())) + data = (epsilon, self.identity, self.baseline_revision, self.engine.model_revision, + self.engine.failed, self.engine.raw_mode, selected, self.engine.objective, + self.objective, self.reference, self.prior_full, self.original_anchor, + self.activation_plan, self.restore_envelope, tuple(self.records), tuple(self.checkpoints), + tuple(self.replay_windows), native) + return hashlib.sha256(pickle.dumps(data, protocol=5)).digest() + except (AttributeError, TypeError, ValueError, OverflowError, pickle.PicklingError): + return None # Unsupported cache key means full replay, never readiness. + + def reuse_subset_causal_features(self, original, *, epsilon=.01): + """Project a verified full FD matrix only for the identical owned subtrial. + + This transfers numeric columns, never a native verdict, shadow token, + profile, validation, lease, or optimizer choice. Any unknown case rebuilds. + """ + import pickle + import struct + from types import MappingProxyType + from openpilot.starpilot.flm.canonical_surface_adapter import NAMES, parameters_for + from openpilot.starpilot.flm.event_activation import EventActivationPlan, ZERO_REFERENCE_ACTIVATION_VERSION + from openpilot.starpilot.flm.event_objective import EffectObjective + from openpilot.starpilot.flm.selected_effects import EffectTrial, EffectReplayTransition + + try: + if (type(self) is not EventControllerReplay or type(original) is not EventControllerReplay or self is original or + 'engine' not in self.__dict__ or 'engine' not in original.__dict__): + return False + if (type(epsilon) is not float or not math.isfinite(epsilon) or not 0 < epsilon <= .03 or + self.restore_envelope is not None or original.restore_envelope is not None or + self.engine is not original.engine or + self.failed or original.failed or not self.raw_mode or not original.raw_mode or + any(getattr(self, name) is not getattr(original, name) for name in + ('factory', 'records', 'checkpoints', 'replay_windows', 'emissions', 'activation_plan')) or + type(self.objective) is not EffectObjective or type(original.objective) is not EffectObjective or + self.objective.group != original.objective.group or self.objective.context_key != original.objective.context_key): + return False + if self.activation_plan is not None and (type(self.activation_plan) is not EventActivationPlan or + self.activation_plan.version != ZERO_REFERENCE_ACTIVATION_VERSION): + return False + names, full_names = self.objective.names, original.objective.names + if (not set(names) < set(full_names) or self.objective.parameters != parameters_for(names) or + original.objective.parameters != parameters_for(full_names)): + return False + if (self.__causal_features is not None or self.projected or + pickle.dumps((self.identity, self.baseline_revision), protocol=5) != + pickle.dumps((original.identity, original.baseline_revision), protocol=5)): + return False + saved = original.__causal_features + old_key = original._causal_input_key(epsilon) + if (old_key is None or type(saved) is not tuple or len(saved) != 2 or saved[0] != old_key or + type(saved[1]) is not MappingProxyType): + return False + + def bits(values): + if type(values) is not dict or tuple(values) != NAMES or any(type(values[n]) is not float for n in NAMES): + raise ValueError('subset requires exact builtin full30 float prior') + return struct.pack('!30d', *(values[n] for n in NAMES)) + + before = EffectTrial(original.objective, original.reference, original.baseline_revision, original.original_anchor) + after = EffectTrial(self.objective, self.reference, self.baseline_revision, self.original_anchor) + _, _, old_prior = before.basis() + _, _, new_prior = after.basis() + prior_bits = bits(old_prior) + if any(bits(value) != prior_bits for value in (new_prior, original.prior_full, self.prior_full)): + return False + for engine in (original, self): + reference = tuple(engine.prior_full[name] for name in engine.objective.names) + if (type(engine.reference) is not tuple or any(type(value) is not float for value in engine.reference) or + struct.pack('!' + 'd' * len(reference), *engine.reference) != struct.pack('!' + 'd' * len(reference), *reference)): + return False + # The only allowed anchor-payload difference declares the smaller names. + old_anchor, new_anchor = dict(original.original_anchor), dict(self.original_anchor) + old_anchor.pop('anchor_digest') + new_anchor.pop('anchor_digest') + if pickle.dumps(old_anchor, protocol=5) != pickle.dumps(new_anchor, protocol=5): + return False + indices = tuple(full_names.index(name) for name in names) + # Independently validate BOTH bases/targets/static full30 source geometry + # for each exact clamped probe. Same original trace, plan, checkpoint and + # prepare clocks then give identical step_full paths by induction. + for index, parameter in enumerate(self.objective.parameters): + for direction in (-1, 1): + full = list(original.reference) + subset = list(self.reference) + probe = max(parameter.low, min(parameter.high, self.reference[index] + direction * epsilon)) + full[indices[index]], subset[index] = probe, probe + left = EffectTrial(original.objective, tuple(full), self.baseline_revision, original.original_anchor) + right = EffectTrial(self.objective, tuple(subset), self.baseline_revision, self.original_anchor) + left_transition, right_transition = EffectReplayTransition(left), EffectReplayTransition(right) + left_source, right_source = left.source(), right.source() + if (bits(left_transition.original_target) != bits(right_transition.original_target) or + bits(left_transition.target) != bits(right_transition.target) or + bits(left_source.values) != bits(right_source.values) or + left_source.full_profile_sha256 != right_source.full_profile_sha256 or + left_source.original_effect_anchor != original.objective.anchor_digest or + right_source.original_effect_anchor != self.objective.anchor_digest): + return False + copied = {} + for stamp, (baseline, features) in saved[1].items(): + if (type(stamp) is not int or type(features) is not tuple or len(features) != len(full_names) or + any(type(value) is not float or not math.isfinite(value) for value in (baseline, *features))): + return False + copied[stamp] = baseline, tuple(features[index] for index in indices) + if not copied or original._causal_input_key(epsilon) != old_key: + return False + new_key = self._causal_input_key(epsilon) + if new_key is None: + return False + self.__causal_features = new_key, MappingProxyType(copied) + self.projected = dict(copied) + # ONLY after every original subset/full30/source proof succeeded, rebind + # the controller-only ORIGINAL prefix. _run consumes the common base + # engine objective, recorded source and inputs before activation; trial + # names/coordinates/transition are unused there. Native replay stays fresh. + # Prefix keys ALWAYS use .01, independently from an FD caller's epsilon. + from openpilot.starpilot.flm.controller_replay import ControllerReplay, Snapshot + from openpilot.starpilot.flm.torque_emission_replay import EmissionHistory + prefix = original.__dict__.get('_EventControllerReplay__controller_prefix') + if (epsilon == .01 and type(self.engine) is ControllerReplay and type(self.emissions) is EmissionHistory and + type(self.activation_plan) is EventActivationPlan and self.activation_plan.version == ZERO_REFERENCE_ACTIVATION_VERSION and + self.__dict__.get('_EventControllerReplay__controller_prefix') is None and + type(prefix) is tuple and len(prefix) == 4 and prefix[0] == old_key and + all(type(part) is MappingProxyType for part in prefix[1:]) and + prefix[1].keys() == prefix[2].keys() and + all(type(stamp) is int and type(value) is float and math.isfinite(value) for stamp, value in prefix[1].items()) and + all(type(value) is bool for value in prefix[2].values()) and + all(window in self.replay_windows and type(value) is Snapshot for window, value in prefix[3].items())): + self.__controller_prefix = (new_key, *(MappingProxyType(dict(part)) for part in prefix[1:])) + return True + except (AttributeError, TypeError, ValueError, KeyError, OverflowError, pickle.PicklingError): + return False + + def ensure_causal_features(self, *, epsilon=.01): + """Reuse only this engine's successful original full rebuild; never IPC flags.""" + if self.restore_envelope is not None: + return self.rebuild_causal_features(epsilon=epsilon) + key = self._causal_input_key(epsilon) + if key is None or self.__causal_features is None or self.__causal_features[0] != key: + return self.rebuild_causal_features(epsilon=epsilon) + # Restore only the readonly derived result, never a caller-edited projection. + self.projected = dict(self.__causal_features[1]) + return len(self.projected) def rebuild_causal_features(self, *, epsilon=.01): + self.__causal_features = None if self.restore_envelope is not None: raise ValueError('restoration requires joint full-map/group-off proof, not marker finite differences') if not math.isfinite(epsilon) or not 0 < epsilon <= .03: @@ -219,8 +585,35 @@ class EventControllerReplay: continue sensitivities.append({stamp: (high[stamp] - low[stamp]) / span for stamp in baseline}) self.projected = {stamp: (value, tuple(feature[stamp] for feature in sensitivities)) for stamp, value in baseline.items()} + from types import MappingProxyType + key = self._causal_input_key(epsilon) + if key is not None: + self.__causal_features = key, MappingProxyType(dict(self.projected)) return len(self.projected) + def native_feasible(self, coordinates): + """Child trial-only feasibility, without losses, physical targets or gains. + + This is not a ReplayValidation and never grants application authority. + Full validate() repeats all guards and measures heldout benefit separately. + """ + from openpilot.starpilot.flm.event_activation import ZERO_REFERENCE_ACTIVATION_VERSION + if (self.activation_plan is None or self.activation_plan.version != ZERO_REFERENCE_ACTIVATION_VERSION or + self.restore_envelope is not None): + raise ValueError('native feasibility requires declared trial plan') + try: + # The strict rollout cache performs full owned-input binding. Do not keep + # a second, weaker plan/reference-only baseline memo here. + # A rejected candidate cannot grant feasibility; avoid an unnecessary + # baseline lookup/hash before that strict candidate rejection. + trial_requests, proposed, trial_saturated = self._rollout(coordinates) + base_requests, baseline, base_saturated = self._rollout(self.reference) + return bool(set(proposed) == set(baseline) and + not any(trial_saturated[stamp] and not base_saturated[stamp] for stamp in base_requests) and + self._slew_ok(base_requests, trial_requests, encoded=False) and self._slew_ok(baseline, proposed, encoded=True)) + except (ValueError, TypeError, KeyError, AttributeError, OverflowError): + return False + def validate(self, profile, control, output, physical_context, blocks, model): if (self.failed or profile.policy != (RESTORE_POLICY if self.restore_envelope is not None else POLICY) or self.restore_envelope is not None and profile.effect_envelope != self.restore_envelope or profile.identity != self.identity or diff --git a/openpilot/starpilot/flm/event_evaluate.py b/openpilot/starpilot/flm/event_evaluate.py index b69c8ec0b0..7aba6e86b6 100644 --- a/openpilot/starpilot/flm/event_evaluate.py +++ b/openpilot/starpilot/flm/event_evaluate.py @@ -6,6 +6,7 @@ from openpilot.starpilot.flm.adaptive_profile import Result, evaluate, loss from openpilot.starpilot.flm.event_controller_replay import POLICY TRIAL_STEP = .005 +NATIVE_FEASIBILITY_SCALES = (1., .5, .25, .125, .0625, .03125, .015625, .0078125) def _lower(values): @@ -17,7 +18,7 @@ def _lower(values): def evaluate_event(identity, parameters, blocks, *, episodes, now_ns, required_cohorts, response_revision, - sequence=1, ridge=.01, prior=None, effect_envelope=None): + sequence=1, ridge=.01, prior=None, effect_envelope=None, native_feasible=None): """Actual fit boundary; caller reconstructs episodes from exact child trace. An episode crossing train/heldout boundaries is rejected, not counted twice. @@ -68,6 +69,26 @@ def evaluate_event(identity, parameters, blocks, *, episodes, now_ns, required_c for old, target in zip(reference, fitted.profile.coefficients, strict=True)) if proposal == reference: return Result('collecting', 'no_identifiable_event_trial') + if native_feasible is not None: + # The direction was fitted ONCE on train. Existing ridge-heldout acceptance + # above remains a conservative preliminary gate, never a direction selector. + # Select ONLY by original whole native/source/quality/slew feasibility; no + # target, sample loss or heldout gain is supplied to this callback. + delta = tuple(target - old for old, target in zip(reference, proposal, strict=True)) + for scale in NATIVE_FEASIBILITY_SCALES: + candidate = tuple(old + scale * step for old, step in zip(reference, delta, strict=True)) + if candidate == reference: + continue # Finite grid underflow cannot manufacture a neutral profile. + feasible = native_feasible(candidate) + if type(feasible) is not bool: + return Result('unavailable', 'event_native_feasibility_contract') + if feasible: + proposal = candidate + break + else: + return Result('collecting', 'event_no_native_feasible_scale') + # This selected proposal is now frozen. ALL following tiny-loss/heldout + # checks run once; failure MUST NOT retry a smaller scale. heldout = blocks[split:] improvements = [] for cohort in sorted(required_cohorts): diff --git a/openpilot/starpilot/flm/event_fit_factory.py b/openpilot/starpilot/flm/event_fit_factory.py index 12e5e255a3..f51da5e303 100644 --- a/openpilot/starpilot/flm/event_fit_factory.py +++ b/openpilot/starpilot/flm/event_fit_factory.py @@ -46,11 +46,11 @@ def _identified(engine, raw): def fit_event_snapshot(payload, factory, replay_payload): - if type(payload.get('event_version')) is not int or payload['event_version'] not in (2, 3, 4, 5): + if type(payload.get('event_version')) is not int or payload['event_version'] not in (2, 3, 4, 5, 6): raise ValueError('explicit event snapshot version') event_clipping_quality(payload) envelope = envelope_from_dict(payload['effect_envelope']) - if payload['event_version'] in (4, 5) and (payload.get('event_operation') != 'restore' or envelope.operation != 'restore'): + if payload['event_version'] in (4, 5, 6) and (payload.get('event_operation') != 'restore' or envelope.operation != 'restore'): raise ValueError('whole-map activation is restore-only') if payload['event_version'] == 3 and (payload.get('event_operation') != 'trial' or envelope.operation != 'trial'): raise ValueError('declared activation requires original trial operation') @@ -63,13 +63,13 @@ def fit_event_snapshot(payload, factory, replay_payload): replay_payload['objective'] = asdict(legacy) actual = ControllerReplay.from_snapshot(factory, legacy, pack_primitive_snapshot(replay_payload), rebuild=False) if envelope.operation == 'restore': - if payload.get('event_version') not in (2, 4, 5): + if payload.get('event_version') not in (2, 4, 5, 6): raise ValueError('declared activation cannot replace full-map restore contract') if payload.get('event_operation') != 'restore': raise ValueError('explicit historical restore job required') from openpilot.starpilot.flm.event_restore_fit import fit_event_restore, prepare_fixed_restore_calibration engine = EventControllerReplay(actual, objective, envelope.replay_anchor(), restore_envelope=envelope) - if payload['event_version'] in (4, 5): + if payload['event_version'] in (4, 5, 6): from openpilot.starpilot.flm.restore_activation import build_restore_plan from openpilot.starpilot.flm.runtime_adapter_registry import canonical_physical_context prepared = prepare_fixed_restore_calibration(payload, engine, objective) @@ -103,8 +103,10 @@ def fit_event_snapshot(payload, factory, replay_payload): if selected != objective.names: envelope = envelope.for_trial(selected) objective = EffectObjective(objective.group, selected, envelope.trial_anchor, objective.context_key) + previous = engine engine = EventControllerReplay(actual, objective, envelope.replay_anchor(), activation_plan=activation_plan) - engine.rebuild_causal_features() + if payload.get('event_version') != 3 or engine.reuse_subset_causal_features(previous) is not True: + engine.rebuild_causal_features() from openpilot.starpilot.flm.event_raw_fit import fit_event_torque payload = {**payload, 'effect_envelope': asdict(envelope), 'event_objective': asdict(objective), diff --git a/openpilot/starpilot/flm/event_raw_fit.py b/openpilot/starpilot/flm/event_raw_fit.py index d384b8c0d6..b741b53611 100644 --- a/openpilot/starpilot/flm/event_raw_fit.py +++ b/openpilot/starpilot/flm/event_raw_fit.py @@ -143,7 +143,7 @@ def fit_event_torque(payload, engine, objective): break if model is None: return _unavailable('event_independent_physical_model', wholeBlocks=len(events), modelPoints=len(rows)) - engine.rebuild_causal_features() + engine.ensure_causal_features() blocks, stage_events = [], [] unexcited_stage_blocks = 0 previous = cutoff @@ -176,9 +176,13 @@ def fit_event_torque(payload, engine, objective): return _unavailable('event_disjoint_stage_support', wholeBlocks=len(events), stageBlocks=len(blocks), unexcitedStageBlocks=unexcited_stage_blocks) episodes = tracking_episodes(engine, tuple(stage_events), model, objective) prior = None if payload.get('prior') is None else tuple(payload['prior']) + from openpilot.starpilot.flm.event_activation import ZERO_REFERENCE_ACTIVATION_VERSION + native_feasible = (engine.native_feasible if engine.activation_plan is not None and + engine.activation_plan.version == ZERO_REFERENCE_ACTIVATION_VERSION else None) result = evaluate_event(engine.identity, objective.parameters, tuple(blocks), episodes=episodes, now_ns=payload['now_ns'], required_cohorts=objective.required_cohorts, - response_revision='event-independent-physical-v2', sequence=payload['sequence'], prior=prior, effect_envelope=envelope) + response_revision='event-independent-physical-v2', sequence=payload['sequence'], prior=prior, + effect_envelope=envelope, native_feasible=native_feasible) validation = None if result.profile is not None: current, applied = control_from_dict(payload['control']), output_from_dict(payload['output']) diff --git a/openpilot/starpilot/flm/event_restore_fit.py b/openpilot/starpilot/flm/event_restore_fit.py index c519c5d01a..eebded1336 100644 --- a/openpilot/starpilot/flm/event_restore_fit.py +++ b/openpilot/starpilot/flm/event_restore_fit.py @@ -66,8 +66,8 @@ def _restore_original_evidence(payload, engine, objective): def prepare_fixed_restore_calibration(payload, engine, objective): from openpilot.starpilot.flm.clipping_quality import event_clipping_quality - if type(payload.get('event_version')) is not int or payload['event_version'] not in (4, 5): - raise ValueError('explicit fixed-map restore version4/5') + if type(payload.get('event_version')) is not int or payload['event_version'] not in (4, 5, 6): + raise ValueError('explicit fixed-map restore version4/5/6') event_clipping_quality(payload) envelope, context, packets, events = _restore_original_evidence(payload, engine, objective) _control_frame_index(engine.records) # reject ambiguous original activation clocks @@ -94,7 +94,7 @@ def _fixed_restore_rollout(payload, engine, coordinates, *, restore_target=False try: return engine._rollout(coordinates, restore_target=restore_target, restore_omit_group=restore_omit_group) except ValueError as error: - if payload.get('event_version') not in (4, 5): + if payload.get('event_version') not in (4, 5, 6): raise return unavailable('restore_actual_full_map_controller_replay', status='rejected', event_version=payload['event_version'], replay_phase='group-off' if restore_omit_group is not None else 'full-map' if restore_target else 'baseline', @@ -106,7 +106,7 @@ def fit_event_restore(payload, engine, objective): frames = _control_frame_index(engine.records) if len(events) < 16: return unavailable('restore_whole_physical_event_support', wholeBlocks=len(events), requiredWholeBlocks=16) - declared = payload.get('event_version') in (4, 5) + declared = payload.get('event_version') in (4, 5, 6) if declared: from openpilot.starpilot.flm.event_activation import bind_activation_model from openpilot.starpilot.flm.restore_activation import build_restore_plan @@ -195,14 +195,14 @@ def fit_event_restore(payload, engine, objective): for group in effects: # Joint group impact must be present in this actual supported trajectory. # No unobservable saved low-speed/window coordinate restores on faith. - if payload.get('event_version') == 5: + if payload.get('event_version') in (5, 6): group_gains[group].append(sum(group_errors[group]) / len(samples) - after_loss) elif sum(value > 1e-8 for value in signals[group]) < 40: group_gains[group].append(None) else: group_gains[group].append(sum(group_errors[group]) / len(samples) - after_loss) middle = len(blocks) // 2 - if payload.get('event_version') == 5: + if payload.get('event_version') in (5, 6): from openpilot.starpilot.flm.restore_support import capped_group_credits, independent_half_supported, all_event_lower for group, trace in effects.items(): evidence = tuple((event['block_id'], tuple((point['control_ns'], point['apply_ns'], @@ -217,7 +217,7 @@ def fit_event_restore(payload, engine, objective): support_credits=half, required_credits=80, required_effectful_events=2) for group, gains in group_gains.items(): for cohort in (gains[:middle], gains[middle:]): - if payload.get('event_version') == 5: + if payload.get('event_version') in (5, 6): if all_event_lower(cohort, _lower) is None: return unavailable('restore_group_independent_heldout_support', group=group) else: diff --git a/openpilot/starpilot/flm/fit_worker.py b/openpilot/starpilot/flm/fit_worker.py index 36af0bc6d9..04e1a148d8 100644 --- a/openpilot/starpilot/flm/fit_worker.py +++ b/openpilot/starpilot/flm/fit_worker.py @@ -1,5 +1,6 @@ """Fresh bounded numerical/replay actor. Invoked only by optional learner owner.""" +import gc import json import os import resource @@ -33,7 +34,17 @@ def main(): snapshot = json.loads(raw) from openpilot.starpilot.flm.learner_factory import fit_snapshot - result = fit_snapshot(snapshot) + # One isolated, address-space/CPU-bounded job. Reference counting remains + # active; avoid repeated cycle scans of the same large replay input graph. + was_enabled = gc.isenabled() + try: + gc.disable() + result = fit_snapshot(snapshot) + finally: + if was_enabled: + gc.enable() + else: + gc.disable() encoded = json.dumps(result, separators=(',', ':'), allow_nan=False).encode() if len(encoded) > MAX_OUTPUT: raise ValueError('fit result bound') diff --git a/openpilot/starpilot/flm/learner_factory.py b/openpilot/starpilot/flm/learner_factory.py index 64ed7fc999..ea95240f3a 100644 --- a/openpilot/starpilot/flm/learner_factory.py +++ b/openpilot/starpilot/flm/learner_factory.py @@ -447,7 +447,7 @@ def fit_snapshot(snapshot): if payload.get('version') != 1 or payload.get('family') != snapshot.get('family'): raise ValueError('fit family') event_version = payload.get('event_version') - event_job = type(event_version) is int and event_version in (2, 3, 4, 5) + event_job = type(event_version) is int and event_version in (2, 3, 4, 5, 6) if 'event_version' in payload and not event_job: raise ValueError('unknown explicit torque event evidence version') from openpilot.starpilot.flm.clipping_quality import event_clipping_quality diff --git a/openpilot/starpilot/flm/pid_controller_replay.py b/openpilot/starpilot/flm/pid_controller_replay.py index 4fcff81dea..585f3d37f4 100644 --- a/openpilot/starpilot/flm/pid_controller_replay.py +++ b/openpilot/starpilot/flm/pid_controller_replay.py @@ -87,15 +87,11 @@ class PidControllerFactory: return controller, VehicleModel(self.cp) -def run_pid(controller, vm, inputs, *, effect_trial=None): - from opendbc.car import structs - from openpilot.starpilot.flm.controller_replay import _CS_FIELDS, _VM_FIELDS +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 = structs.CarState.new_message() - for name in _CS_FIELDS: - setattr(cs, name, bool(values[name]) if name in ('steeringPressed', 'standstill', 'steerFaultTemporary', 'steerFaultPermanent') else values[name]) - cs.gearShifter = int(values['gearShifter']) + 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']) diff --git a/openpilot/starpilot/flm/restore_activation.py b/openpilot/starpilot/flm/restore_activation.py index 161c25da4f..fc8d9aad10 100644 --- a/openpilot/starpilot/flm/restore_activation.py +++ b/openpilot/starpilot/flm/restore_activation.py @@ -4,7 +4,8 @@ import hashlib import json from openpilot.starpilot.flm.clipping_quality import ( - FIXED_MAP_IDENTIFICATION, RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION, QUALIFIED_EVENT_CLIPPING_QUALITY, + FIXED_MAP_IDENTIFICATION, RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION, RESTORE_ZERO_REFERENCE_VERSION, + restore_policy, restore_quality, ) from openpilot.starpilot.flm.event_activation import EventActivationPlan, EXACT_EMISSION_ACTIVATION_VERSION, build_activation_plan from openpilot.starpilot.flm.full_surface_profile import FullSurfaceEnvelope @@ -22,7 +23,8 @@ class WholeMapRestorePlan(EventActivationPlan): restore_envelope_sha256: str def validate(self): - if (self.version not in (RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION) or type(self.restore_envelope_sha256) is not str or + versions = (RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION, RESTORE_ZERO_REFERENCE_VERSION) + if (self.version not in versions or type(self.restore_envelope_sha256) is not str or len(self.restore_envelope_sha256) != 64 or any(c not in '0123456789abcdef' for c in self.restore_envelope_sha256)): raise ValueError('whole-map restore plan version/envelope') # Reuse only the chronology/lineage validator; never decode this as a trial. @@ -33,7 +35,7 @@ class WholeMapRestorePlan(EventActivationPlan): @property def clipping_quality(self): self.validate() - return QUALIFIED_EVENT_CLIPPING_QUALITY + return restore_quality(6 if self.version == RESTORE_ZERO_REFERENCE_VERSION else 4) @property def identification_policy(self): @@ -58,13 +60,13 @@ def restore_plan_matches(plan, envelope): def build_restore_plan(engine, *, model, cutoff_ns, stage_events, physical_context, envelope, restore_version=4): - if type(restore_version) is not int or restore_version not in (4, 5): + if type(restore_version) is not int or restore_version not in (4, 5, 6): raise ValueError('explicit whole-map restore support version') - policy = {4: RESTORE_ACTIVATION_VERSION, 5: RESTORE_AGGREGATE_ACTIVATION_VERSION}[restore_version] + policy = restore_policy(restore_version) digest = restore_envelope_digest(envelope) if engine.restore_envelope is None or restore_envelope_digest(engine.restore_envelope) != digest: raise ValueError('whole-map restore declaration changed transaction') original = build_activation_plan(engine, model=model, cutoff_ns=cutoff_ns, stage_events=stage_events, - physical_context=physical_context, quality_contract=QUALIFIED_EVENT_CLIPPING_QUALITY) + physical_context=physical_context, quality_contract=restore_quality(restore_version)) return WholeMapRestorePlan(**{**asdict(original), 'version': policy}, restore_envelope_sha256=digest).validate() diff --git a/openpilot/starpilot/flm/selected_effects.py b/openpilot/starpilot/flm/selected_effects.py index fad2726c6d..2288ec6bc1 100644 --- a/openpilot/starpilot/flm/selected_effects.py +++ b/openpilot/starpilot/flm/selected_effects.py @@ -439,11 +439,13 @@ class EffectReplayTransition: self.target.update(zip(self.names, trial.coordinates, strict=True)) self.applied = None self.previous_ns = 0 + self.__source_key = self.__source = None def reset(self): self.applied = None self.previous_ns = 0 self.recorded_effects_seen = False + self.__source_key = self.__source = None def source_for_frame(self, control_ns, trace=None, *, new_owner=False): from openpilot.starpilot.flm.effect_trace import step_full @@ -478,8 +480,17 @@ class EffectReplayTransition: self.applied = step_full(self.applied, self.target, now_ns=control_ns, previous_ns=self.previous_ns, qualified=qualified) self.previous_ns = control_ns source = EffectReplaySource.__new__(EffectReplaySource) - group = {n: self.applied[n] for n in self.names} - source.source = CanonicalSurfaceEffects(group, self.trial.selected_baseline_revision, accumulated=self.applied) + # Per-transition geometry only: trace validation and exact step_full above + # still run EVERY frame. Packed doubles preserve signed zero and full30. + import struct + key = self.trial.selected_baseline_revision, self.names, struct.pack('!30d', *(self.applied[n] for n in NAMES)) + if key != self.__source_key: + group = {n: self.applied[n] for n in self.names} + self.__source = CanonicalSurfaceEffects(group, self.trial.selected_baseline_revision, accumulated=self.applied) + self.__source_key = key + source.source = self.__source + # The ordinary _run -> begin_frame path still resets ALL call counters and + # enforces the real frame clock. No mutable frame state crosses transitions. source.original_effect_anchor = source.source.original_effect_anchor source.applied_coordinates = source.source.applied_coordinates return source diff --git a/openpilot/starpilot/flm/steerlearnerd.py b/openpilot/starpilot/flm/steerlearnerd.py index 3bd8c4ba82..635cf228c0 100644 --- a/openpilot/starpilot/flm/steerlearnerd.py +++ b/openpilot/starpilot/flm/steerlearnerd.py @@ -25,15 +25,15 @@ def production_adapter_for_evidence(cp, evidence, key, *, source): """Adopt the qualified event contract only in the optional live producer. Legacy/reference callers retain version2. The live event producer explicitly - selects the separate fixed-map version5 restore declaration, never FD shadow. + selects the separate fixed-map version6 restore declaration, never FD shadow. """ - from openpilot.starpilot.flm.clipping_quality import QUALIFIED_EVENT_CLIPPING_QUALITY + from openpilot.starpilot.flm.clipping_quality import ZERO_REFERENCE_QUALITY from openpilot.starpilot.flm.runtime_adapter_registry import SourceState, adapter_for_evidence if not isinstance(source, SourceState) or not source.enabled or source.manual: return None return adapter_for_evidence(cp, evidence, key, source=source, - event_replay_version=3, event_clipping_quality=QUALIFIED_EVENT_CLIPPING_QUALITY, event_restore_version=5) + event_replay_version=3, event_clipping_quality=ZERO_REFERENCE_QUALITY, event_restore_version=6) def run( diff --git a/openpilot/starpilot/flm/tests/test_declared_restore_production.py b/openpilot/starpilot/flm/tests/test_declared_restore_production.py index 1b2264c1a4..a2066b6c5c 100644 --- a/openpilot/starpilot/flm/tests/test_declared_restore_production.py +++ b/openpilot/starpilot/flm/tests/test_declared_restore_production.py @@ -5,6 +5,7 @@ from opendbc.car.honda.values import CAR as HONDA from opendbc.car.hyundai.values import CAR as HKG from opendbc.car.toyota.values import CAR as TOYOTA from openpilot.starpilot.flm.learner_factory import from_evidence +from openpilot.starpilot.flm.clipping_quality import ZERO_REFERENCE_QUALITY, QUALIFIED_EVENT_CLIPPING_QUALITY from openpilot.starpilot.flm.runtime_adapter_registry import baseline_values from openpilot.starpilot.flm.steerlearnerd import production_adapter_for_evidence from openpilot.starpilot.flm.tests.test_production_event_contract import capture @@ -20,9 +21,14 @@ def test_actual_registered_producer_explicit_restore_version_and_legacy_default( before = controls.CP.as_builder().to_bytes(), baseline_values(controls.LaC) live = production_adapter_for_evidence(controls.CP, evidence, ('event', 'center'), source=source) old = from_evidence(controls.CP, evidence, ('event', 'center'), source=source) - assert live is not None and live.replay_version == 3 and live.restore_version == 5 + assert live is not None and live.replay_version == 3 and live.restore_version == 6 + assert live.clipping_quality == ZERO_REFERENCE_QUALITY + for version in (4, 5): + legacy = from_evidence(controls.CP, evidence, ('event', 'center'), source=source, + event_replay_version=3, event_clipping_quality=QUALIFIED_EVENT_CLIPPING_QUALITY, event_restore_version=version) + assert legacy is not None and legacy.restore_version == version and legacy.clipping_quality == QUALIFIED_EVENT_CLIPPING_QUALITY assert old is not None and old.replay_version == 2 and old.restore_version == 2 assert from_evidence(controls.CP, evidence, ('event', 'center'), source=source, event_restore_version=4) is None live.select_objective(('event', 'deadband.high')) - assert live.restore_version == 5 + assert live.restore_version == 6 assert (controls.CP.as_builder().to_bytes(), baseline_values(controls.LaC)) == before diff --git a/openpilot/starpilot/flm/tests/test_event_controller_prefix.py b/openpilot/starpilot/flm/tests/test_event_controller_prefix.py new file mode 100644 index 0000000000..3f56a53398 --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_event_controller_prefix.py @@ -0,0 +1,311 @@ +"""Controller-only preactivation reuse: actual selected controllers and native CAN.""" +from dataclasses import replace +from types import ModuleType, SimpleNamespace +from unittest import mock +import pickle +import sys + +import pytest + +from openpilot.starpilot.flm.controller_replay import ( + ControllerReplay, ReplayFrame, SelectedControllerFactory, _run, capture_snapshot, final_command, frame_input, +) +from openpilot.starpilot.flm.effect_trace import FullEffectTrace +from openpilot.starpilot.flm.event_activation import EventActivationPlan, ZERO_REFERENCE_ACTIVATION_VERSION +from openpilot.starpilot.flm.event_controller_replay import EventControllerReplay +from openpilot.starpilot.flm.event_objective import EffectObjective +from openpilot.starpilot.flm.pid_controller_replay import PidControllerFactory +from openpilot.starpilot.flm.selected_effects import EffectReplayRecordedSource +from openpilot.starpilot.flm.torque_adapter import Objective +from openpilot.starpilot.flm.torque_emission_replay import Emission +from openpilot.starpilot.flm.tests.test_full_surface_profile import base +from openpilot.starpilot.flm.tests.test_event_lazy_native import ( + TOYOTA, HYUNDAI, HONDA, make_interface, install_observer, settle, control, apply, BASE_NS, +) + + +def fixture(domain='torque', nonzero=False, two_windows=False): + platform = {'torque': TOYOTA.TOYOTA_COROLLA_TSS2, + 'starpilot': HYUNDAI.HYUNDAI_IONIQ_6, 'pid': HONDA.HONDA_CIVIC}[domain] + ci = make_interface(platform) + assert install_observer(ci) + settle(ci, control(0.)) + factory = PidControllerFactory(ci.CP, 'native') if domain == 'pid' else SelectedControllerFactory( + ci.CP, 'starpilot' if domain == 'starpilot' else 'standard') + objective = Objective('feedforward', 'high', 'steady') + original = ControllerReplay(factory, objective, identity='f' * 64, baseline_revision='a' * 64) + actual, vm = factory.create() + original.raw_mode = True + first = BASE_NS + ci.CC.frame * 10_000_000 + original.checkpoint(first, capture_snapshot(actual, vm)) + envelope = base() + if nonzero: + envelope = base(prior=base().with_proposal({'ff_gain_left': .002})) + rows = [] + for tick in range(60): + stamp = first + tick * 10_000_000 + if two_windows and tick == 30: + original.checkpoint(stamp, capture_snapshot(actual, vm)) + trace = FullEffectTrace(1, envelope, stamp, stamp - 10_000_000, True, + envelope.coordinates, envelope.coordinates, envelope.coordinates, envelope.full_profile_sha256).validate() + inputs = frame_input(ci.CS.out, vm, SimpleNamespace(angleOffsetDeg=.01, roll=.002), active=True, + safety_limited=False, desired_curvature=.00003, curvature_limited=False, lat_delay=.2) + inputs += (('control_frame', float(tick + 1)), ('raw_model_curvature', .00003)) + value, _ = _run(actual, vm, inputs, objective, (0., 0.), effect_trial=EffectReplayRecordedSource(trace)) + rows.append(ReplayFrame(stamp, inputs, final_command(value), (0., 0.), frame_sequence=tick + 1, + actual_lac_command=value, controller_domain='torque_pid' if domain == 'pid' else 'torque', + effect_trace=trace, effect_prepare_ns=stamp)) + _, packets, witness = apply(ci, control(value)) + if witness is not None: + assert (witness.can_address, witness.can_data, witness.can_bus) in packets + clipped = witness.unlimited_raw is None or witness.unlimited_raw != witness.final_raw + event = Emission(stamp, stamp, stamp + 1_000_000, witness, 'f' * 64, 'e' * 64, 'a' * 64, + True, True, False, False, clipped, False, False, 0, 0, '') + original.emissions.events[event.apply_ns] = event + original.emissions.by_control[event.control_ns] = event + original.records.extend(rows) + original.replay_windows = (((rows[0].control_ns, rows[29].control_ns), (rows[30].control_ns, rows[-1].control_ns)) + if two_windows else ((rows[0].control_ns, rows[-1].control_ns),)) + selected = EffectObjective('ff.left', envelope.trial_names, envelope.trial_anchor, envelope.context_key) + activation = rows[45 if two_windows else 30].control_ns + windows = (((rows[0].control_ns, rows[29].control_ns, 0, 0), + (rows[30].control_ns, rows[-1].control_ns, activation, activation)) if two_windows else + ((first, rows[-1].control_ns, activation, activation),)) + plan = EventActivationPlan(ZERO_REFERENCE_ACTIVATION_VERSION, original.identity, original.baseline_revision, + selected.context_key, 'b' * 64, tuple('train-' + str(i) for i in range(4)), + tuple('held-' + str(i) for i in range(4)), activation - 10_000_000, + tuple('stage-' + str(i) for i in range(9)), windows).validate() + return EventControllerReplay(original, selected, envelope.replay_anchor(), activation_plan=plan) + + +def state(value, seen=None): + """Independent vars/containers audit, not capture_snapshot against itself.""" + seen = set() if seen is None else seen + if value is None or type(value) in (bool, int, float, str, bytes): + return value + label = type(value).__module__ + '.' + type(value).__qualname__ + # Constructor bindings are identities, not mutable numerical state objects. + # In particular, memoryview.tolist on the CLASS is an unbound descriptor. + if isinstance(value, ModuleType): + return ('module', value.__name__, getattr(value, '__file__', None)) + if isinstance(value, type): + return ('class', value.__module__, value.__qualname__, getattr(sys.modules.get(value.__module__), '__file__', None)) + if callable(value): + return ('callable', getattr(value, '__module__', None), getattr(value, '__qualname__', None), label, + getattr(sys.modules.get(getattr(value, '__module__', None)), '__file__', None)) + if id(value) in seen: + return ('reference', label) + seen.add(id(value)) + if isinstance(value, dict): + return label, tuple((state(k, seen), state(v, seen)) for k, v in value.items()) + if isinstance(value, (tuple, list)) or label == 'collections.deque': + return label, tuple(state(v, seen) for v in value), getattr(value, 'maxlen', None) + if hasattr(value, 'to_dict'): + return label, value.to_dict() + if hasattr(value, 'tolist'): + return label, value.tolist() + if hasattr(value, '__dict__'): + return label, tuple((k, state(v, seen)) for k, v in vars(value).items()) + return label, str(value) + + +def observed(engine, coordinates, shadow=True): + calls, native = [], [] + original_native = engine.emissions.rollout + def update(*args, **kwargs): + result = _run(*args, **kwargs) + calls.append((args[2], result[0], result[1].to_dict(), state(args[0]), state(args[1]))) + return result + def emit(requests, *args, **kwargs): + result = original_native(requests, *args, **kwargs) + native.append((dict(requests), result, kwargs)) + return result + token = {'_identification_token': engine._EventControllerReplay__identification_token} if shadow else {} + with mock.patch('openpilot.starpilot.flm.event_controller_replay._run', side_effect=update), \ + mock.patch.object(engine.emissions, 'rollout', side_effect=emit): + result = engine._rollout(coordinates, **token) + return result, calls, native + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +@pytest.mark.parametrize('nonzero', (False, True)) +@pytest.mark.parametrize('shadow, delta', ((False, .000000001), (True, .000000001), (True, .01), (True, -.01))) +def test_actual_prefix_reuse_full_tail_state_logs_requests_and_native_maps_exact(domain, nonzero, shadow, delta): + cached = fixture(domain, nonzero) + # Original full strict baseline must succeed before a certificate can exist. + baseline = cached._rollout(cached.reference) + certificate = cached._EventControllerReplay__controller_prefix + assert certificate is not None and len(certificate[1]) == 30 + # The original guard checks raw Float64, NOT the rounded native request map. + assert all(pickle.dumps(certificate[1][row.control_ns], protocol=5) == + pickle.dumps(row.actual_lac_command, protocol=5) for row in list(cached.records)[:30]) + with pytest.raises(TypeError): + certificate[1][0] = 0. + eager = EventControllerReplay(cached.engine, cached.objective, cached.original_anchor, activation_plan=cached.activation_plan) + coordinates = (cached.reference[0] + delta,) + expected, before, native_before = observed(eager, coordinates, shadow=shadow) + actual, after, native_after = observed(cached, coordinates, shadow=shadow) + assert pickle.dumps(actual, protocol=5) == pickle.dumps(expected, protocol=5) + assert len(before) == 60 and len(after) == 30 + assert after == before[30:] # Every actual treated log + independent vars(controller/VM). + assert native_before == native_after and len(native_after[0][0]) == 60 + assert set(actual[0]) == set(baseline[0]) + + +@pytest.mark.parametrize('change', ('input', 'checkpoint', 'plan', 'prior_bits', 'native', 'context', 'late_fault')) +def test_mutated_owned_inputs_never_use_stale_prefix(change): + engine = fixture() + engine._rollout(engine.reference) + before = engine._EventControllerReplay__controller_prefix + if change == 'input': + row = engine.records[0] + engine.records[0] = replace(row, inputs=row.inputs + (('extra', 1.),)) + elif change == 'checkpoint': + stamp, snapshot = engine.checkpoints[0] + engine.checkpoints[0] = (stamp, replace(snapshot, values=snapshot.values + (('unknown', 1.),))) + elif change == 'plan': + engine.activation_plan = replace(engine.activation_plan, physical_context='c' * 64) + elif change == 'prior_bits': + engine.prior_full['ff_gain_right'] = -0. + elif change == 'native': + stamp, event = next(iter(engine.emissions.events.items())) + engine.emissions.events[stamp] = replace(event, witness=event.witness._replace(final_raw=event.witness.final_raw + 1)) + elif change == 'context': + engine.factory.manual_source_epoch = 'changed' + else: + engine.records[-1] = replace(engine.records[-1], healthy=False) + assert engine._causal_input_key(.01) != before[0] + with mock.patch('openpilot.starpilot.flm.event_controller_replay._run', wraps=_run) as run: + try: + engine._rollout((.00001,), _identification_token=engine._EventControllerReplay__identification_token) + except (ValueError, KeyError, TypeError): + pass + assert run.call_count > 0 or change in ('checkpoint', 'context') + + +def test_failure_unknown_operation_and_bad_shadow_token_never_create_certificate(): + engine = fixture() + stamp, event = next(iter(engine.emissions.events.items())) + engine.emissions.events[stamp] = replace(event, witness=event.witness._replace(final_raw=event.witness.final_raw + 1)) + with pytest.raises(ValueError): + engine._rollout(engine.reference) + assert engine._EventControllerReplay__controller_prefix is None + engine = fixture() + with pytest.raises(ValueError, match='identification scope'): + engine._rollout((.00001,), _identification_token=object()) + assert engine._EventControllerReplay__controller_prefix is None + # Old/default operation has no new certificate even after a complete baseline. + engine.activation_plan = None + engine._rollout(engine.reference) + assert engine._EventControllerReplay__controller_prefix is None + + +def test_every_original_prefix_context_guard_and_late_tail_guard_still_runs(): + engine = fixture() + engine._rollout(engine.reference) + calls = [] + original = engine.engine._frame_context + def context(frame): + calls.append(frame.control_ns) + return original(frame) + with mock.patch.object(engine.engine, '_frame_context', side_effect=context): + engine._rollout((.00001,), _identification_token=engine._EventControllerReplay__identification_token) + assert calls == [frame.control_ns for frame in engine.records] + engine.records[-1] = replace(engine.records[-1], healthy=False) + with pytest.raises(ValueError, match='source/history'): + engine._rollout((.00001,), _identification_token=engine._EventControllerReplay__identification_token) + + +def test_baseline_source_changed_during_success_never_publishes_pending_certificate(): + engine = fixture() + with mock.patch.object(engine, '_causal_input_key', side_effect=(b'first', b'changed')): + with pytest.raises(ValueError, match='changed owned input'): + engine._rollout(engine.reference) + assert engine._EventControllerReplay__controller_prefix is None + assert engine._EventControllerReplay__pending_controller_prefix is None + + +def test_shadow_prefix_source_changed_during_success_rejects_and_discards_certificate(): + engine = fixture() + engine._rollout(engine.reference) + key = engine._EventControllerReplay__controller_prefix[0] + with mock.patch.object(engine, '_causal_input_key', side_effect=(key, b'changed')): + with pytest.raises(ValueError, match='changed owned input'): + engine._rollout((.000000001,), _identification_token=engine._EventControllerReplay__identification_token) + assert engine._EventControllerReplay__controller_prefix is None + + +def test_restore_endpoint_never_uses_trial_prefix_certificate(): + engine = fixture() + engine._rollout(engine.reference) + with pytest.raises(ValueError, match='restore endpoint'): + engine._rollout(engine.reference, restore_target=True) + + +def test_unavailable_activation_snapshot_keeps_original_baseline_and_creates_no_certificate(): + engine = fixture() + with mock.patch('openpilot.starpilot.flm.event_controller_replay.capture_snapshot', + side_effect=ValueError('unsupported transient state')): + result = engine._rollout(engine.reference) + assert len(result[0]) == 60 + assert engine._EventControllerReplay__controller_prefix is None + + +def test_unsupported_exact_controller_policy_keeps_original_replay_without_certificate(): + engine = fixture('starpilot') + original_create = engine.factory.create + def create(): + controller, vm = original_create() + # Inherit all actual methods/state but leave the audited exact-class set. + original = type(controller.starpilot_extension) + controller.starpilot_extension.__class__ = type('UnknownExtension', (original,), {}) + return controller, vm + with mock.patch.object(engine.factory, 'create', side_effect=create): + result = engine._rollout(engine.reference) + assert len(result[0]) == 60 + assert engine._EventControllerReplay__controller_prefix is None + + +def test_actual_raw_double_prefix_guard_preserves_non_float32_exact_command(): + engine = fixture() + assert any(row.actual_lac_command != final_command(row.actual_lac_command) for row in list(engine.records)[:30]) + engine._rollout(engine.reference) + certificate = engine._EventControllerReplay__controller_prefix + assert any(value != final_command(value) for value in certificate[1].values()) + result, tail, _ = observed(engine, (.000000001,)) + assert len(result[0]) == 60 and len(tail) == 30 + + +def test_legacy_shell_without_new_private_fields_keeps_original_argument_api(): + from openpilot.starpilot.flm.tests.test_event_strict_rollout_cache import engine, numeric + value = engine() + with mock.patch.object(value, '_rollout_uncached', side_effect=numeric) as original: + value._rollout(value.reference) + assert original.call_args.kwargs == {} + value.activation_plan = None + value._rollout((.01,)) + assert original.call_args.kwargs == {'restore_target': False, 'restore_omit_group': None, '_identification_token': None} + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +def test_actual_second_window_checkpoint_reset_and_native_partition_remain_exact(domain): + cached = fixture(domain, nonzero=True, two_windows=True) + cached._rollout(cached.reference) + certificate = cached._EventControllerReplay__controller_prefix + assert certificate is not None and len(certificate[1]) == 15 and len(certificate[3]) == 1 + eager = EventControllerReplay(cached.engine, cached.objective, cached.original_anchor, activation_plan=cached.activation_plan) + expected, before, native_before = observed(eager, (cached.reference[0] + .01,)) + actual, after, native_after = observed(cached, (cached.reference[0] + .01,)) + assert pickle.dumps(actual, protocol=5) == pickle.dumps(expected, protocol=5) + assert len(before) == 60 and len(after) == 45 + assert after == before[:30] + before[45:] + assert native_after == native_before + + +def test_state_observer_handles_module_class_and_memoryview_instance_without_dropping_values(): + import math + assert state(memoryview) == ('class', 'builtins', 'memoryview', None) + assert state(math) == ('module', 'math', getattr(math, '__file__', None)) + assert state(memoryview(b'ab')) == ('builtins.memoryview', [97, 98]) + assert state(SimpleNamespace(number=3., mutable=[1., 2.], constructor=memoryview)) != \ + state(SimpleNamespace(number=3., mutable=[1., 4.], constructor=memoryview)) diff --git a/openpilot/starpilot/flm/tests/test_event_frame_index.py b/openpilot/starpilot/flm/tests/test_event_frame_index.py index c739150d8d..19b7678dc4 100644 --- a/openpilot/starpilot/flm/tests/test_event_frame_index.py +++ b/openpilot/starpilot/flm/tests/test_event_frame_index.py @@ -49,7 +49,7 @@ def test_actual_fit_missing_stage_frame_is_fail_closed(): 'control': {'stamp': index * 1_000_000_000}, 'output': {'stamp': index * 1_000_000_000 + 1}, 'physical': {'lateral_accel': .02}},)} for index in range(1, 17)) first_stage = events[8]['samples'][0] - engine = SimpleNamespace(records=(), identity=model.identity, projected={}, rebuild_causal_features=lambda: None) + engine = SimpleNamespace(records=(), identity=model.identity, projected={}, ensure_causal_features=lambda: None) objective = SimpleNamespace(names=('center',), required_cohorts=('center',)) envelope = SimpleNamespace(prior_coordinates=(('center', 0.),)) with mock.patch('openpilot.starpilot.flm.event_raw_fit.original_event_evidence', diff --git a/openpilot/starpilot/flm/tests/test_event_lazy_native.py b/openpilot/starpilot/flm/tests/test_event_lazy_native.py new file mode 100644 index 0000000000..b0d0f2a0ea --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_event_lazy_native.py @@ -0,0 +1,181 @@ +"""Exact selected-controller/CAN replay units, never native/profile authority.""" +from dataclasses import replace +from types import SimpleNamespace +from unittest import mock + +import pytest + +from opendbc.car.honda.values import CAR as HONDA +from opendbc.car.hyundai.values import CAR as HYUNDAI +from opendbc.car.toyota.values import CAR as TOYOTA +from openpilot.starpilot.flm.controller_replay import ( + ControllerReplay, ReplayFrame, SelectedControllerFactory, _run, capture_snapshot, final_command, frame_input, +) +from openpilot.starpilot.flm.event_controller_replay import EventControllerReplay, _LazyControllerRequests +from openpilot.starpilot.flm.event_objective import EffectObjective +from openpilot.starpilot.flm.pid_controller_replay import PidControllerFactory +from openpilot.starpilot.flm.torque_adapter import Objective +from openpilot.starpilot.flm.torque_emission_replay import Emission +from openpilot.starpilot.flm.tests.test_full_surface_profile import base +from openpilot.starpilot.flm.tests.test_steering_projection import make_interface, install_observer, settle, control, apply, BASE_NS + + +class EagerRequests(_LazyControllerRequests): + """Test-only original ordering: ALL controller work before native rollout.""" + def __init__(self, values, frames): + super().__init__(values, frames) + self.finish() + + +def replay_fixture(domain): + platform = {'torque': TOYOTA.TOYOTA_COROLLA_TSS2, + 'starpilot': HYUNDAI.HYUNDAI_IONIQ_6, 'pid': HONDA.HONDA_CIVIC}[domain] + ci = make_interface(platform) + assert install_observer(ci) + settle(ci, control(0.)) + factory = PidControllerFactory(ci.CP, 'native') if domain == 'pid' else SelectedControllerFactory( + ci.CP, 'starpilot' if domain == 'starpilot' else 'standard') + objective = Objective('feedforward', 'high', 'steady') + original = ControllerReplay(factory, objective, identity='f' * 64, baseline_revision='a' * 64) + actual, vm = factory.create() + original.raw_mode = True + first = BASE_NS + ci.CC.frame * 10_000_000 + original.checkpoint(first, capture_snapshot(actual, vm)) + rows = [] + for tick in range(44): + stamp = first + tick * 10_000_000 + inputs = frame_input(ci.CS.out, vm, SimpleNamespace(angleOffsetDeg=0., roll=0.), active=True, + safety_limited=False, desired_curvature=.00003, curvature_limited=False, lat_delay=.2) + inputs += (('control_frame', float(tick + 1)), ('raw_model_curvature', .00003)) + value, _ = _run(actual, vm, inputs, objective, (0., 0.)) + rows.append(ReplayFrame(stamp, inputs, final_command(value), (0., 0.), frame_sequence=tick + 1, + actual_lac_command=value, controller_domain='torque_pid' if domain == 'pid' else 'torque')) + if tick >= 40: + continue # Controller-only tail MUST still run after native success. + _, packets, witness = apply(ci, control(value)) + if witness is not None: + assert (witness.can_address, witness.can_data, witness.can_bus) in packets + clipped = witness.unlimited_raw is None or witness.unlimited_raw != witness.final_raw + event = Emission(stamp, stamp, stamp + 1_000_000, witness, 'f' * 64, 'e' * 64, 'a' * 64, + True, True, False, False, clipped, False, False, 0, 0, '') + # As in registered codec units, native flags grant NO current authority. + # We exercise original decode/EncoderBranch with actual captured bytes. + original.emissions.events[event.apply_ns] = event + original.emissions.by_control[event.control_ns] = event + original.records.extend(rows) + original.replay_windows = ((rows[0].control_ns, rows[-1].control_ns),) + envelope = base() + selected = EffectObjective('ff.left', envelope.trial_names, envelope.trial_anchor, envelope.context_key) + return EventControllerReplay(original, selected, envelope.replay_anchor()) + + +def run_observed(engine, eager=False, coordinates=(0.,)): + trace = [] + engine.emissions.decoder = None # Both paths independently decode real bytes. + def update(*args, **kwargs): + result = _run(*args, **kwargs) + trace.append((args[2], result[0], result[1].to_dict(), capture_snapshot(args[0], args[1]))) + return result + with mock.patch('openpilot.starpilot.flm.event_controller_replay._run', side_effect=update), \ + mock.patch('openpilot.starpilot.flm.event_controller_replay._LazyControllerRequests', + EagerRequests if eager else _LazyControllerRequests): + try: + return ('success', engine._rollout_uncached(coordinates)), trace + except ValueError as error: + return ('rejected', str(error)), trace + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +@pytest.mark.parametrize('coordinates', ((0.,), (.00001,))) +def test_actual_selected_and_native_success_outputs_and_full_controller_trace_equal(domain, coordinates): + engine = replay_fixture(domain) + eager, before = run_observed(engine, eager=True, coordinates=coordinates) + lazy, after = run_observed(engine, coordinates=coordinates) + assert lazy == eager + if lazy[0] == 'success': + assert after == before and len(after) == len(engine.records) + assert set(lazy[1][0]) == {row.control_ns for row in engine.records} + if coordinates == (0.,): + assert lazy[0] == 'success' + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +def test_real_first_native_byte_failure_rejects_without_unneeded_controller_tail(domain): + engine = replay_fixture(domain) + stamp, event = next(iter(engine.emissions.events.items())) + engine.emissions.events[stamp] = replace(event, witness=event.witness._replace(final_raw=event.witness.final_raw + 1)) + eager, before = run_observed(engine, eager=True) + lazy, after = run_observed(engine) + assert eager == lazy and lazy[0] == 'rejected' and 'actual steering CAN bytes mismatch' in lazy[1] + assert 0 < len(after) < len(before) == len(engine.records) + + +@pytest.mark.parametrize('defect', ('late_controller', 'missing_last_tick', 'missing_consumed_request')) +def test_native_success_never_skips_late_controller_or_exact_endpoint_guards(defect): + engine = replay_fixture('torque') + if defect == 'late_controller': + engine.records[-1] = replace(engine.records[-1], healthy=False) + elif defect == 'missing_last_tick': + first, last = engine.replay_windows[0] + engine.engine.replay_windows = ((first, last + 10_000_000),) + else: + stamp, event = next(iter(engine.emissions.events.items())) + engine.emissions.events[stamp] = replace(event, control_ns=event.control_ns + 5_000_000) + eager, _ = run_observed(engine, eager=True) + lazy, _ = run_observed(engine) + assert lazy == eager and lazy[0] == 'rejected' + + +def test_lazy_mapping_repeated_older_keys_missing_key_and_explicit_finish(): + values, generated = {}, [] + def frames(): + for stamp in (10, 20, 30): + generated.append(stamp) + values[stamp] = stamp / 100 + yield stamp + lazy = _LazyControllerRequests(values, frames()) + assert 20 in lazy and generated == [10, 20] + assert lazy[10] == .1 and lazy[20] == .2 and generated == [10, 20] + lazy.finish() + assert generated == [10, 20, 30] and dict(lazy) == values + assert 99 not in lazy + with pytest.raises(KeyError): + lazy[99] + + +@pytest.mark.parametrize('defect', ('success', 'native_failure', 'tail_failure')) +def test_controller_iterator_is_closed_on_every_exit_without_bypassing_finish(defect): + engine = replay_fixture('torque') + if defect == 'native_failure': + stamp, event = next(iter(engine.emissions.events.items())) + engine.emissions.events[stamp] = replace(event, witness=event.witness._replace(final_raw=event.witness.final_raw + 1)) + elif defect == 'tail_failure': + engine.records[-1] = replace(engine.records[-1], healthy=False) + closed = [] + original_close = _LazyControllerRequests.close + def close(value): + original_close(value) + closed.append(value.frames.gi_frame is None) + with mock.patch.object(_LazyControllerRequests, 'close', close): + result, trace = run_observed(engine) + assert closed == [True] + assert result[0] == ('success' if defect == 'success' else 'rejected') + if defect == 'success': + assert len(trace) == len(engine.records) + + +def test_unknown_legacy_receiver_keeps_plain_dictionary_and_eager_order(): + from openpilot.starpilot.flm.tests.test_declared_restore import replay_fixture as legacy_fixture + engine, restored, frames, emissions = legacy_fixture() + coordinates = tuple(dict(restored.coordinates)[name] for name in engine.objective.names) + seen = [] + def update(*args, **kwargs): + seen.append(args[2]) + return 0., SimpleNamespace(saturated=False) + def native(requests, *args, **kwargs): + assert type(requests) is dict and len(requests) == len(frames) and len(seen) == len(frames) + return {} + emissions.rollout.side_effect = native + with mock.patch('openpilot.starpilot.flm.event_controller_replay._run', side_effect=update), \ + mock.patch('openpilot.starpilot.flm.event_controller_replay.restore_snapshot'): + engine._rollout_uncached(coordinates, restore_target=True) diff --git a/openpilot/starpilot/flm/tests/test_event_native_backtrack.py b/openpilot/starpilot/flm/tests/test_event_native_backtrack.py new file mode 100644 index 0000000000..0a05c2a238 --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_event_native_backtrack.py @@ -0,0 +1,143 @@ +"""Deterministic native selection, no heldout search or authority fabrication.""" +from dataclasses import asdict, replace +from unittest import mock + +import pytest + +from openpilot.starpilot.flm.adaptive_profile import Block, Sample, loss +from openpilot.starpilot.flm.canonical_surface_adapter import parameters_for +from openpilot.starpilot.flm.event_evaluate import evaluate_event, NATIVE_FEASIBILITY_SCALES +from openpilot.starpilot.flm.event_controller_replay import EventControllerReplay +from openpilot.starpilot.flm.event_activation import ( + ZERO_REFERENCE_ACTIVATION_VERSION, EXACT_EMISSION_ACTIVATION_VERSION, activation_from_dict, + activation_model_from_dict, activation_matches_model, bind_activation_model, +) +from openpilot.starpilot.flm.tests.test_full_surface_profile import base +from openpilot.starpilot.flm.tests.test_event_activation import primitive_plan, fitted_model +from openpilot.starpilot.flm.tests.test_declared_restore import envelope, plan_for + + +def problem(): + envelope = base() + identity, cohort, response = 'b' * 64, 'cohort', 'measured-response' + blocks, episodes = [], [] + for episode in range(9): + samples = tuple(Sample(1_000_000 * (episode * 40 + row + 1) + 2, + 1_000_000 * (episode * 40 + row + 1), 1_000_000 * (episode * 40 + row + 1) + 1, + (1.,), .02, 1.) for row in range(40)) + block = Block(identity, str(episode), cohort, 'torque-selected-effect-v2', response, samples) + blocks.append(block) + episodes.append({'id': f'{episode:064x}', 'kind': 'subtle', 'start_ns': samples[0].control_ns, + 'end_ns': samples[-1].mono_ns, 'block_ids': (block.block_id,)}) + kwargs = {'episodes': tuple(episodes), 'now_ns': 1_000_000_000, 'required_cohorts': (cohort,), 'response_revision': response, + 'prior': (0.,), 'effect_envelope': envelope} + return (identity, parameters_for(envelope.trial_names), tuple(blocks)), kwargs + + +def test_first_native_feasible_scale_selected_before_tiny_heldout_losses_exact_metrics(): + args, kwargs = problem() + calls = [] + def native(coordinates): + calls.append(coordinates) + return len(calls) == 3 + def exact_loss(samples, coordinates): + assert len(calls) == 3 # All event tiny-heldout losses are AFTER frozen selection. + return loss(samples, coordinates) + with mock.patch('openpilot.starpilot.flm.event_evaluate.loss', side_effect=exact_loss): + result = evaluate_event(*args, **kwargs, native_feasible=native) + assert NATIVE_FEASIBILITY_SCALES == (1., .5, .25, .125, .0625, .03125, .015625, .0078125) + assert calls == [(.005,), (.0025,), (.00125,)] + profile = result.profile + assert profile is not None and profile.coefficients == calls[-1] + rows = tuple(row for block in args[2][4:] for row in block.samples) + assert profile.baseline_loss == loss(rows, (0.,)) + assert profile.candidate_loss == loss(rows, calls[-1]) + assert profile.candidate_loss != loss(rows, calls[0]) * .25 # Never scale fake losses. + assert dict(profile.effect_envelope.coordinates)['ff_gain_left'] == calls[-1][0] + assert profile.train_ids == ('0', '1', '2', '3') and profile.heldout_ids == ('4', '5', '6', '7', '8') + + +def test_all_fixed_native_scales_fail_without_losses_neutral_or_profile(): + args, kwargs = problem() + native = mock.Mock(return_value=False) + with mock.patch('openpilot.starpilot.flm.event_evaluate.loss') as tiny_loss: + result = evaluate_event(*args, **kwargs, native_feasible=native) + assert result.profile is None and result.reason == 'event_no_native_feasible_scale' + assert [call.args[0] for call in native.call_args_list] == [(.005 * scale,) for scale in NATIVE_FEASIBILITY_SCALES] + tiny_loss.assert_not_called() + + +def test_selected_tiny_heldout_failure_never_falls_back_or_retries(): + args, kwargs = problem() + native = mock.Mock(side_effect=(False, True, AssertionError('must not retry heldout rejection'))) + with mock.patch('openpilot.starpilot.flm.event_evaluate._lower', return_value=-1.): + result = evaluate_event(*args, **kwargs, native_feasible=native) + assert result.profile is None and result.reason == 'event_tiny_trial_heldout_regression_or_noise' + assert native.call_count == 2 + + +def test_preliminary_gate_failure_cannot_select_direction_and_legacy_has_no_callback(): + args, kwargs = problem() + native = mock.Mock(return_value=True) + # Real contradictory heldout data fails the existing preliminary ridge gate. + blocks = tuple(replace(block, samples=tuple(replace(row, residual_target=-.02) for row in block.samples)) + if index >= 4 else block for index, block in enumerate(args[2])) + result = evaluate_event(args[0], args[1], blocks, **kwargs, native_feasible=native) + assert result.profile is None and 'heldout' in result.reason + native.assert_not_called() + ordinary = evaluate_event(*args, **kwargs) + assert ordinary.profile is not None and ordinary.profile.coefficients == (.005,) + assert evaluate_event(*args, **kwargs, native_feasible=lambda _: 1).reason == 'event_native_feasibility_contract' + + +def replay(): + plan = replace(primitive_plan(fitted_model()), version=ZERO_REFERENCE_ACTIVATION_VERSION).validate() + engine = EventControllerReplay.__new__(EventControllerReplay) + engine.activation_plan, engine.restore_envelope, engine.reference = plan, None, (0.,) + engine._EventControllerReplay__strict_rollouts = None + return engine + + +def test_native_selector_has_no_loss_inputs_and_unknown_source_runs_baseline_each_time(): + engine = replay() + native = ({1: 0., 2: 0.}, {1: 0., 2: 0.}, {1: False, 2: False}) + seen = [] + def rollout(coordinates): + seen.append(coordinates) + return native + with mock.patch.object(engine, '_rollout', side_effect=rollout), \ + mock.patch.object(engine, '_slew_ok', return_value=True) as slew: + assert engine.native_feasible((.005,)) and engine.native_feasible((.0025,)) + assert seen == [(.005,), (0.,), (.0025,), (0.,)] + assert slew.call_count == 4 + # This symbolic shell has no owned native history and cannot gain cache + # readiness. Actual Torque/PID cache keys and immutable maps are tested in + # test_event_strict_rollout_cache, using their registered factories. + assert engine._EventControllerReplay__strict_rollouts is None + + +@pytest.mark.parametrize('defect', ('clipping', 'saturation', 'request_slew', 'encoded_slew', 'missing')) +def test_native_selector_preserves_every_strict_feasibility_failure(defect): + engine = replay() + baseline = ({1: 0.}, {1: 0.}, {1: False}) + candidate = ({1: .01}, {1: .01}, {1: defect == 'saturation'}) + if defect == 'missing': + candidate = ({1: .01}, {}, {1: False}) + sides = [ValueError('original unclip candidate clipping') if defect == 'clipping' else candidate, baseline] + with mock.patch.object(engine, '_rollout', side_effect=sides), \ + mock.patch.object(engine, '_slew_ok', side_effect=lambda *_, encoded: not ( + defect == 'request_slew' and not encoded or defect == 'encoded_slew' and encoded)): + assert not engine.native_feasible((.005,)) + + +def test_new_unpublished_plan_rejects_abandoned_string_restore_and_legacy_no_backtrack(): + engine = replay() + model = bind_activation_model(fitted_model(), engine.activation_plan) + assert activation_matches_model(engine.activation_plan, activation_model_from_dict(asdict(model))) + abandoned = {**asdict(engine.activation_plan), 'version': 'fixed-eight-shadow-zero-reference-v5'} + with pytest.raises(ValueError): + activation_from_dict(abandoned) + for plan in (replace(engine.activation_plan, version=EXACT_EMISSION_ACTIVATION_VERSION), plan_for(envelope())): + engine.activation_plan = plan + with pytest.raises(ValueError, match='declared trial plan'): + engine.native_feasible((.005,)) diff --git a/openpilot/starpilot/flm/tests/test_event_replay_cost.py b/openpilot/starpilot/flm/tests/test_event_replay_cost.py new file mode 100644 index 0000000000..71e8b5a7c7 --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_event_replay_cost.py @@ -0,0 +1,187 @@ +"""Per-job deterministic reuse only; no manufactured physical/native acceptance.""" +from dataclasses import replace +from types import SimpleNamespace +from unittest import mock + +import pytest +from opendbc.car.car_helpers import interfaces +from opendbc.car.honda.values import CAR as HONDA +from opendbc.car.toyota.values import CAR as TOYOTA +from openpilot.starpilot.flm.canonical_surface_adapter import parameters_for, NAMES +from openpilot.starpilot.flm.controller_replay import SelectedControllerFactory +from openpilot.starpilot.flm.pid_controller_replay import PidControllerFactory +from openpilot.starpilot.flm.event_controller_replay import EventControllerReplay +from openpilot.starpilot.flm.event_objective import EffectObjective +from openpilot.starpilot.flm.selected_effects import EffectTrial, EffectReplayTransition, CanonicalSurfaceEffects +from openpilot.starpilot.flm.tests.test_full_surface_profile import base + + +def engine_for(domain): + if domain == 'pid': + cp = interfaces[HONDA.HONDA_CIVIC].get_non_essential_params(HONDA.HONDA_CIVIC) + factory = PidControllerFactory(cp, 'native') + else: + cp = interfaces[TOYOTA.TOYOTA_RAV4_TSS2].get_non_essential_params(TOYOTA.TOYOTA_RAV4_TSS2) + factory = SelectedControllerFactory(cp, 'standard') + frame = SimpleNamespace(control_ns=1, inputs=(('safety_limited', False),)) + actual = SimpleNamespace(factory=factory, identity='a' * 64, baseline_revision='b' * 64, + model_revision='c' * 64, failed=False, raw_mode=True, objective=('original',), records=[frame], + checkpoints=((1, ('exact-original-state',)),), replay_windows=((1, 2),), + emissions=SimpleNamespace(cp=factory.cp, identity='a' * 64, baseline_revision='b' * 64, + events={1: ('original-native-event',)}, carriers={1: (1, 1)})) + engine = EventControllerReplay.__new__(EventControllerReplay) + engine.engine = actual + engine.objective = SimpleNamespace(names=('ff_gain_left',), parameters=parameters_for(('ff_gain_left',))) + engine.reference, engine.prior_full, engine.original_anchor = (0.,), {'ff_gain_left': 0.}, {'full30': (0.,)} + engine.activation_plan = engine.restore_envelope = None + engine.projected = {} + engine._EventControllerReplay__causal_features = None + return engine + + +def rollout(coordinates): + # Symbolic deterministic native trace for cache behavior, NEVER a validation. + return {1: coordinates[0]}, {1: coordinates[0]}, {1: False} + + +@pytest.mark.parametrize('domain', ('torque', 'pid')) +def test_real_registered_factory_first_builds_then_reuses_exact_readonly_derived_features(domain): + engine = engine_for(domain) + assert type(engine._causal_input_key(.01)) is bytes + with mock.patch.object(engine, '_rollout', side_effect=rollout) as native: + assert engine.ensure_causal_features() == 1 and native.call_count == 3 + original = dict(engine.projected) + engine.projected[1] = ('untrusted-public-edit',) + assert engine.ensure_causal_features() == 1 and engine.projected == original + assert native.call_count == 3 + _, cached = engine._EventControllerReplay__causal_features + with pytest.raises(TypeError): + cached[1] = (0., (9.,)) + assert engine.ensure_causal_features(epsilon=.02) == 1 and native.call_count == 6 + + +@pytest.mark.parametrize('field', ('frame', 'native', 'checkpoint', 'anchor', 'objective', 'plan', 'source', 'factory')) +def test_changed_exact_owned_input_does_not_reuse_previous_feature_result(field): + engine = engine_for('torque') + with mock.patch.object(engine, '_rollout', side_effect=rollout) as native: + engine.ensure_causal_features() + if field == 'frame': + engine.records[0].inputs = (('safety_limited', True),) + elif field == 'native': + engine.emissions.events[1] = ('changed-native-event',) + elif field == 'checkpoint': + engine.engine.checkpoints = ((1, ('changed-checkpoint',)),) + elif field == 'anchor': + engine.original_anchor['full30'] = (-0.,) # Bit-sensitive, unlike dataclass equality. + elif field == 'objective': + engine.objective.parameters = tuple(replace(p, excitation_floor=.02) for p in engine.objective.parameters) + elif field == 'plan': + engine.activation_plan = SimpleNamespace(identification_policy='strict-final-quality-v1') + elif field == 'source': + engine.engine.baseline_revision = 'd' * 64 + else: + engine.factory.manual_source_epoch = 'changed-source' + engine.ensure_causal_features() + assert native.call_count == 6 + + +def test_boolean_integer_type_and_missing_factory_cache_keys_never_alias_readiness(): + engine = engine_for('pid') + before = engine._causal_input_key(.01) + engine.records[0].inputs = (('safety_limited', 0),) + assert engine._causal_input_key(.01) != before + engine.engine.factory = SimpleNamespace() + assert engine._causal_input_key(.01) is None + with mock.patch.object(engine, '_rollout', side_effect=rollout) as native: + engine.ensure_causal_features() + engine.ensure_causal_features() + assert native.call_count == 6 + + +def test_failed_full_build_cannot_create_ready_result_and_restore_still_rejects(): + engine = engine_for('pid') + with mock.patch.object(engine, '_rollout', side_effect=ValueError('native/source guard')): + with pytest.raises(ValueError): + engine.ensure_causal_features() + assert engine._EventControllerReplay__causal_features is None + engine.restore_envelope = object() + with pytest.raises(ValueError, match='restoration requires'): + engine.ensure_causal_features() + + +def transition(coordinate=.005): + envelope = base(('ff_gain_left',)) + objective = EffectObjective('ff.left', envelope.trial_names, envelope.trial_anchor, envelope.context_key) + return EffectReplayTransition(EffectTrial(objective, (coordinate,), envelope.selected_baseline_revision, envelope.replay_anchor())) + + +def test_transition_reuses_only_exact_plateau_geometry_but_resets_each_real_frame_counter(): + owner = transition() + with mock.patch('openpilot.starpilot.flm.selected_effects.CanonicalSurfaceEffects', wraps=CanonicalSurfaceEffects) as constructor: + last = None + for tick in range(110): + current = owner.source_for_frame(1_000_000_000 + tick * 10_000_000) + current.begin_frame(1_000_000_000 + tick * 10_000_000) + current.selected_friction(.1, original_anchor=current.original_effect_anchor) + assert current.friction_calls == 1 + with pytest.raises(ValueError): + current.selected_friction(.1, original_anchor=current.original_effect_anchor) + if tick >= 102: + assert current.source is last.source + last = current + assert constructor.call_count <= 101 # Ramp configs change; plateau configs do not. + previous = last.source + owner.reset() + fresh = owner.source_for_frame(3_000_000_000) + assert fresh.source is not previous + + +def test_cached_and_fresh_geometry_fields_are_bit_exact_and_candidates_do_not_share_state(): + owner = transition(0.) + first = owner.source_for_frame(1_000_000_000) + first.begin_frame(1_000_000_000) + second = owner.source_for_frame(1_010_000_000) + second.begin_frame(1_010_000_000) + assert second.source is first.source + values = dict(owner.applied) + fresh = CanonicalSurfaceEffects({'ff_gain_left': values['ff_gain_left']}, owner.trial.selected_baseline_revision, accumulated=values) + for name in ('values', 'names', 'applied_coordinates', 'original_effect_anchor', 'full_original_anchor', 'full_profile_sha256'): + assert getattr(second.source, name) == getattr(fresh, name) + assert second.source.surface.knobs == fresh.surface.knobs + other = transition(0.).source_for_frame(1_010_000_000) + assert other.source is not second.source + with pytest.raises(ValueError, match='nonmonotonic'): + second.begin_frame(1_010_000_000) + assert tuple(values) == NAMES + + +def test_plateau_cache_keeps_per_frame_trace_validation_and_exact_stage_outputs(): + from openpilot.starpilot.flm.tests.test_canonical_surface_adapter import CanonicalSurfaceTest + owner = transition(.005) + for tick in range(105): + source = owner.source_for_frame(1_000_000_000 + tick * 10_000_000) + source.begin_frame(1_000_000_000 + tick * 10_000_000) + values = dict(owner.applied) + fresh = CanonicalSurfaceEffects({'ff_gain_left': values['ff_gain_left']}, owner.trial.selected_baseline_revision, accumulated=values) + fresh.begin_frame(2_040_000_000) + for speed, setpoint in ((8., .2), (30., -.2)): + frame = CanonicalSurfaceTest().frame(speed=speed, setpoint=setpoint) + assert source.selected_stage(frame, original_anchor=source.original_effect_anchor) == fresh.selected_stage( + frame, original_anchor=fresh.original_effect_anchor) + source.begin_frame(source.source.frame_ns + 1) + fresh.begin_frame(fresh.frame_ns + 1) + invalid = SimpleNamespace(validate=mock.Mock(side_effect=ValueError('invalid current trace'))) + with pytest.raises(ValueError, match='invalid current trace'): + owner.source_for_frame(2_100_000_000, invalid) + invalid.validate.assert_called_once() + + +def test_uninitialized_legacy_shell_never_recurses_or_mints_cache_readiness(): + from openpilot.starpilot.flm.tests.test_event_fd_boundary import engine as legacy_engine + engine = legacy_engine() + assert engine._causal_input_key(.01) is None + with mock.patch.object(engine, '_rollout', side_effect=rollout) as native: + assert engine.ensure_causal_features() == 1 + assert engine.ensure_causal_features() == 1 + assert native.call_count == 6 + assert engine._EventControllerReplay__causal_features is None diff --git a/openpilot/starpilot/flm/tests/test_event_strict_rollout_cache.py b/openpilot/starpilot/flm/tests/test_event_strict_rollout_cache.py new file mode 100644 index 0000000000..c28784a0fc --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_event_strict_rollout_cache.py @@ -0,0 +1,172 @@ +"""Bounded deterministic memo tests, not controller/native/physical qualification.""" +from dataclasses import replace +from types import SimpleNamespace +from unittest import mock + +import pytest + +from openpilot.starpilot.flm.event_activation import ZERO_REFERENCE_ACTIVATION_VERSION, EXACT_EMISSION_ACTIVATION_VERSION +from openpilot.starpilot.flm.event_controller_replay import POLICY +from openpilot.starpilot.flm.tests.test_event_activation import fitted_model, primitive_plan +from openpilot.starpilot.flm.tests.test_event_replay_cost import engine_for, rollout + + +def numeric(coordinates, **_kwargs): + return rollout(coordinates) + + +def engine(domain='torque'): + result = engine_for(domain) + result.activation_plan = replace(primitive_plan(fitted_model()), version=ZERO_REFERENCE_ACTIVATION_VERSION).validate() + result._EventControllerReplay__strict_rollouts = None + return result + + +@pytest.mark.parametrize('domain', ('torque', 'pid')) +def test_exact_owned_registered_factory_baseline_and_last_candidate_only_readonly_copyout(domain): + value = engine(domain) + with mock.patch.object(value, '_rollout_uncached', side_effect=numeric) as original: + before = value._rollout(value.reference) + candidate = value._rollout((.01,)) + assert original.call_count == 2 + before[0][1] = 99. + candidate[1][1] = 99. + assert value._rollout(value.reference) == rollout(value.reference) + assert value._rollout((.01,)) == rollout((.01,)) + assert original.call_count == 2 + _, slots = value._EventControllerReplay__strict_rollouts + assert set(slots) == {'baseline', 'candidate'} + with pytest.raises(TypeError): + slots['baseline'][1][0][1] = 1. + value._rollout((.02,)) + value._rollout((.01,)) # Only the last successful candidate is retained. + assert original.call_count == 4 and len(slots) == 2 + + +@pytest.mark.parametrize('change', ('frame', 'native', 'checkpoint', 'anchor', 'objective', 'plan', 'failed', 'factory', 'reference')) +def test_every_changed_owned_input_misses_and_replaces_all_slots(change): + value = engine() + with mock.patch.object(value, '_rollout_uncached', side_effect=numeric) as original: + value._rollout(value.reference) + value._rollout((.01,)) + if change == 'frame': + value.records[0].inputs = (('safety_limited', 0),) # False and0 must not alias. + elif change == 'native': + value.emissions.events[1] = ('different-native-source',) + elif change == 'checkpoint': + value.engine.checkpoints = ((1, ('different-checkpoint',)),) + elif change == 'anchor': + value.original_anchor['full30'] = (-0.,) + elif change == 'objective': + value.objective.names = ('ff_gain_right',) + elif change == 'plan': + value.activation_plan = replace(value.activation_plan, stage_blocks=tuple(reversed(value.activation_plan.stage_blocks))) + elif change == 'failed': + value.engine.failed = True + elif change == 'factory': + value.factory.manual_source_epoch = 'different-source' + else: + value.reference = (-0.,) + value._rollout((.01,)) + assert original.call_count == 3 + assert set(value._EventControllerReplay__strict_rollouts[1]) == {'candidate'} + + +def test_exact_coordinate_types_signed_zero_unknown_key_and_errors_never_create_alias(): + value = engine() + with mock.patch.object(value, '_rollout_uncached', side_effect=numeric) as original: + for coordinates in ((0.,), (0,), (-0.,)): + value._rollout(coordinates) + assert original.call_count == 3 + with mock.patch.object(value, '_causal_input_key', return_value=None): + value._rollout((.01,)) + value._rollout((.01,)) + assert original.call_count == 5 and value._EventControllerReplay__strict_rollouts is None + with mock.patch.object(value, '_rollout_uncached', side_effect=ValueError('strict native failure')) as failure: + for _ in range(2): + with pytest.raises(ValueError, match='strict native failure'): + value._rollout((.01,)) + assert failure.call_count == 2 + assert not value._EventControllerReplay__strict_rollouts[1] + + +def test_shadow_restore_ablation_old_plan_and_unsupported_types_always_execute_original_path(): + for kwargs in ({'_identification_token': object()}, {'restore_target': True}, {'restore_omit_group': ('ff_gain_left',)}): + value = engine() + with mock.patch.object(value, '_rollout_uncached', side_effect=numeric) as original: + value._rollout(value.reference) + slots = dict(value._EventControllerReplay__strict_rollouts[1]) + value._rollout((.01,), **kwargs) + value._rollout((.01,), **kwargs) + assert original.call_count == 3 and value._EventControllerReplay__strict_rollouts[1] == slots + assert original.call_args.kwargs == {'restore_target': kwargs.get('restore_target', False), + 'restore_omit_group': kwargs.get('restore_omit_group'), '_identification_token': kwargs.get('_identification_token')} + value = engine() + value.activation_plan = replace(value.activation_plan, version=EXACT_EMISSION_ACTIVATION_VERSION) + with mock.patch.object(value, '_rollout_uncached', side_effect=numeric) as original: + value._rollout((.01,)) + value._rollout((.01,)) + assert original.call_count == 2 and value._EventControllerReplay__strict_rollouts is None + value = engine() + with mock.patch.object(value, '_rollout_uncached', side_effect=TypeError('original type rejection')) as original: + for coordinates in ((False,), ['x'], object()): + with pytest.raises(TypeError, match='original type rejection'): + value._rollout(coordinates) + assert original.call_count == 3 and value._EventControllerReplay__strict_rollouts is None + + +def test_midexecution_input_mutation_cannot_be_stored_as_success(): + value = engine() + def changing(coordinates): + value.records[0].inputs = (('changed-during-controller-run', True),) + return rollout(coordinates) + with mock.patch.object(value, '_rollout_uncached', side_effect=changing), pytest.raises(ValueError, match='changed owned input'): + value._rollout((.01,)) + assert value._EventControllerReplay__strict_rollouts is None + + +def test_native_feasibility_and_final_current_guard_are_not_cached_admission(): + value = engine() + with mock.patch.object(value, '_rollout_uncached', side_effect=numeric) as original, \ + mock.patch.object(value, '_slew_ok', return_value=True): + assert value.native_feasible((.01,)) + assert value.native_feasible((.01,)) + assert original.call_count == 2 + # This negative supplies no ReplayValidation: a cached numeric trace cannot + # override the ordinary current-output limiter veto in final validate. + profile = SimpleNamespace(policy=POLICY, identity=value.identity, parameters=value.objective.parameters, coefficients=(.01,)) + model = SimpleNamespace(identity=value.identity, domain='normalized_torque', context='unused') + control = SimpleNamespace(identity=value.identity, baseline_revision=value.baseline_revision) + with mock.patch('openpilot.starpilot.flm.event_controller_replay.canonical_physical_context', return_value='unused'): + assert value.validate(profile, control, SimpleNamespace(limited=True), (), (), model) is None + assert original.call_count == 2 + + +@pytest.mark.parametrize('domain', ('torque', 'pid')) +def test_failed_candidate_skips_baseline_lookup_but_retains_exact_owned_key(domain): + value = engine(domain) + with mock.patch.object(value, '_rollout_uncached', side_effect=numeric): + value._rollout(value.reference) + slots = value._EventControllerReplay__strict_rollouts[1] + baseline = slots['baseline'] + with mock.patch.object(value, '_causal_input_key', wraps=value._causal_input_key) as source_key, \ + mock.patch.object(value, '_rollout_uncached', side_effect=ValueError('strict candidate clipping')) as original, \ + mock.patch.object(value, '_slew_ok') as slew: + assert not value.native_feasible((.01,)) + assert original.call_args_list == [mock.call((.01,))] + assert source_key.call_args_list == [mock.call(.01)] + slew.assert_not_called() + assert slots == {'baseline': baseline} # No failed candidate was stored. + + +@pytest.mark.parametrize('domain', ('torque', 'pid')) +def test_successful_candidate_still_requires_original_baseline_and_all_source_keys(domain): + value = engine(domain) + with mock.patch.object(value, '_causal_input_key', wraps=value._causal_input_key) as source_key, \ + mock.patch.object(value, '_rollout_uncached', + side_effect=[rollout((.01,)), ValueError('invalid baseline')]) as original, \ + mock.patch.object(value, '_slew_ok') as slew: + assert not value.native_feasible((.01,)) + assert original.call_args_list == [mock.call((.01,)), mock.call(value.reference)] + assert source_key.call_args_list == [mock.call(.01)] * 3 + slew.assert_not_called() diff --git a/openpilot/starpilot/flm/tests/test_event_subset_fd.py b/openpilot/starpilot/flm/tests/test_event_subset_fd.py new file mode 100644 index 0000000000..a5dff37c03 --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_event_subset_fd.py @@ -0,0 +1,135 @@ +"""Subset FD equivalence; no manufactured fit, model or current authority.""" +from dataclasses import asdict, replace +from types import SimpleNamespace +from unittest import mock + +import pytest + +from openpilot.starpilot.flm.canonical_surface_adapter import GROUPS, NAMES +from openpilot.starpilot.flm.clipping_quality import ZERO_REFERENCE_QUALITY, NATIVE_SHADOW_IDENTIFICATION +from openpilot.starpilot.flm.event_controller_replay import EventControllerReplay +from openpilot.starpilot.flm.event_fit_factory import fit_event_snapshot +from openpilot.starpilot.flm.event_objective import EffectObjective +from openpilot.starpilot.flm.selected_effects import EffectTrial, EffectReplayTransition +from openpilot.starpilot.flm.tests.test_full_surface_profile import base +from openpilot.starpilot.flm.tests.test_event_lazy_native import replay_fixture +from openpilot.starpilot.flm.torque_adapter import Objective + + +def full_and_subset(domain='torque'): + original = replay_fixture(domain).engine + envelope = base(names=GROUPS['ff.left']) + objective = EffectObjective('ff.left', envelope.trial_names, envelope.trial_anchor, envelope.context_key) + full = EventControllerReplay(original, objective, envelope.replay_anchor()) + full.rebuild_causal_features() + envelope = envelope.for_trial(('ff_gain_left',)) + objective = EffectObjective('ff.left', envelope.trial_names, envelope.trial_anchor, envelope.context_key) + return full, EventControllerReplay(original, objective, envelope.replay_anchor()) + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +def test_actual_full_fd_columns_equal_independent_selected_controller_and_native_rebuild(domain): + full, subset = full_and_subset(domain) + with mock.patch.object(subset, '_rollout', side_effect=AssertionError('transfer cannot replay or mint a native result')): + assert subset.reuse_subset_causal_features(full) + copied = dict(subset.projected) + assert copied == {stamp: (row[0], (row[1][0],)) for stamp, row in full.projected.items()} + assert subset._EventControllerReplay__strict_rollouts is None + frozen = subset._EventControllerReplay__causal_features[1] + with pytest.raises(TypeError): + frozen[0] = (0., (1.,)) + subset.projected.clear() + with mock.patch.object(subset, 'rebuild_causal_features', side_effect=AssertionError('fresh bound key must hit')): + assert subset.ensure_causal_features() == len(copied) + assert subset.projected == copied + independent = EventControllerReplay(subset.engine, subset.objective, subset.original_anchor) + independent.rebuild_causal_features() + assert independent.projected == copied + + +@pytest.mark.parametrize('change', ('context', 'frame', 'native', 'prior_bits', 'mutable_anchor', 'plan', + 'factory', 'engine', 'epsilon', 'missing_cache', 'nonfloat_prior')) +def test_changed_or_unknown_owned_input_cannot_rebind_full_feature_proof(change): + full, subset = full_and_subset() + epsilon = .01 + if change == 'context': + subset.objective = replace(subset.objective, context_key='1' * 64) + elif change == 'frame': + full.records[0] = replace(full.records[0], selected_command=-0.) + elif change == 'native': + stamp, event = next(iter(full.emissions.events.items())) + full.emissions.events[stamp] = replace(event, witness=event.witness._replace(final_raw=event.witness.final_raw + 1)) + elif change == 'prior_bits': + subset.prior_full['ff_gain_left'] = -0. + elif change == 'mutable_anchor': + subset.original_anchor['accumulated'] = list(subset.original_anchor['accumulated']) + elif change == 'plan': + subset.activation_plan = SimpleNamespace(version='unknown') + elif change == 'factory': + full.factory.manual_source_epoch = 'changed-factory' + elif change == 'engine': + subset.engine = SimpleNamespace(**vars(full.engine)) + elif change == 'epsilon': + epsilon = .02 + elif change == 'missing_cache': + full._EventControllerReplay__causal_features = None + else: + full.prior_full['ff_gain_left'] = 0 + assert not subset.reuse_subset_causal_features(full, epsilon=epsilon) + assert subset._EventControllerReplay__causal_features is None and not subset.projected + + +def test_nonzero_float_prior_full30_targets_and_source_geometry_are_subset_invariant(): + prior = dict(base(names=GROUPS['ff.left']).prior_coordinates) + prior['turn_in_boost_left'] = .002 + prior['center_taper_max'] = .003 + prior['ff_gain_right'] = -0. + envelope = base(names=GROUPS['ff.left']) + from openpilot.starpilot.flm.canonical_surface_adapter import full_profile_sha256 + coordinates = tuple((name, prior[name]) for name in NAMES) + digest = full_profile_sha256(envelope.selected_baseline_revision, coordinates) + envelope = replace(envelope, prior_coordinates=coordinates, coordinates=coordinates, + prior_full_profile_sha256=digest, full_profile_sha256=digest).validate() + for direction in (-1, 1): + small = envelope.for_trial(('ff_gain_left',)) + left_objective = EffectObjective('ff.left', envelope.trial_names, envelope.trial_anchor, envelope.context_key) + right_objective = EffectObjective('ff.left', small.trial_names, small.trial_anchor, small.context_key) + full = tuple(prior[name] + direction * .01 if name == 'ff_gain_left' else prior[name] for name in envelope.trial_names) + left = EffectTrial(left_objective, full, envelope.selected_baseline_revision, envelope.replay_anchor()) + right = EffectTrial(right_objective, (direction * .01,), envelope.selected_baseline_revision, small.replay_anchor()) + assert EffectReplayTransition(left).target == EffectReplayTransition(right).target + assert left.source().values == right.source().values + assert left.source().full_profile_sha256 == right.source().full_profile_sha256 + + +@pytest.mark.parametrize('reuse', (True, False, 'untrusted-true')) +def test_actual_factory_uses_transfer_only_for_selected_subset_and_falls_back_exactly(reuse): + envelope = base(names=GROUPS['ff.left']) + objective = EffectObjective('ff.left', envelope.trial_names, envelope.trial_anchor, envelope.context_key) + payload = {'event_version': 3, 'event_operation': 'trial', 'event_clipping_quality': ZERO_REFERENCE_QUALITY, + 'event_identification_policy': NATIVE_SHADOW_IDENTIFICATION, 'effect_envelope': asdict(envelope), + 'event_objective': asdict(objective), 'objective': asdict(Objective('feedforward', 'high', 'steady')), + 'physical_context': ('unit-boundary',)} + # Boundary stubs expose routing only; they return no model/profile/validation. + first, full, subset = mock.Mock(), mock.Mock(), mock.Mock() + subset.reuse_subset_causal_features.return_value = reuse + expected = {'status': 'collecting', 'reason': 'unit-boundary-only'} + with mock.patch('openpilot.starpilot.flm.event_fit_factory.ControllerReplay.from_snapshot') as original, \ + mock.patch('openpilot.starpilot.flm.event_fit_factory.EventControllerReplay', side_effect=(first, full, subset)), \ + mock.patch('openpilot.starpilot.flm.event_raw_fit.prepare_fixed_event_calibration', return_value=(object(), 1, (), ())), \ + mock.patch('openpilot.starpilot.flm.event_activation.build_activation_plan', return_value=object()), \ + mock.patch('openpilot.starpilot.flm.runtime_adapter_registry.canonical_physical_context', return_value='unit'), \ + mock.patch('openpilot.starpilot.flm.event_fit_factory._identified', return_value=('ff_gain_left',)), \ + mock.patch('openpilot.starpilot.flm.event_raw_fit.fit_event_torque', return_value=expected) as fit: + assert fit_event_snapshot(payload, object(), {'baseline_revision': envelope.selected_baseline_revision}) == expected + subset.reuse_subset_causal_features.assert_called_once_with(full) + assert subset.rebuild_causal_features.call_count == (0 if reuse is True else 1) + assert full.rebuild_causal_features.call_count == 1 + assert fit.call_args.args[1] is subset and original.call_args.kwargs == {'rebuild': False} + + +def test_unowned_legacy_shells_do_not_proxy_recurse_or_gain_readiness(): + source = EventControllerReplay.__new__(EventControllerReplay) + destination = EventControllerReplay.__new__(EventControllerReplay) + assert not destination.reuse_subset_causal_features(source) + assert not destination.reuse_subset_causal_features(object()) diff --git a/openpilot/starpilot/flm/tests/test_event_subset_prefix.py b/openpilot/starpilot/flm/tests/test_event_subset_prefix.py new file mode 100644 index 0000000000..bbad144a0f --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_event_subset_prefix.py @@ -0,0 +1,105 @@ +"""Subset controller prefix only: no native-result/token/admission transfer.""" +from dataclasses import replace +import pickle +from types import MappingProxyType +from unittest import mock + +import pytest + +from openpilot.starpilot.flm.canonical_surface_adapter import GROUPS +from openpilot.starpilot.flm.controller_replay import _run +from openpilot.starpilot.flm.event_controller_replay import EventControllerReplay +from openpilot.starpilot.flm.event_objective import EffectObjective +from openpilot.starpilot.flm.tests.test_event_controller_prefix import fixture, observed + + +def full_and_subset(domain='torque', nonzero=True, epsilon=.01): + original = fixture(domain, nonzero=nonzero) + envelope = original.records[0].effect_trace.envelope.for_trial(GROUPS['ff.left']) + objective = EffectObjective('ff.left', envelope.trial_names, envelope.trial_anchor, envelope.context_key) + full = EventControllerReplay(original.engine, objective, envelope.replay_anchor(), activation_plan=original.activation_plan) + full.rebuild_causal_features(epsilon=epsilon) + small = envelope.for_trial(('ff_gain_left',)) + selected = EffectObjective('ff.left', small.trial_names, small.trial_anchor, small.context_key) + subset = EventControllerReplay(original.engine, selected, small.replay_anchor(), activation_plan=original.activation_plan) + return full, subset + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +@pytest.mark.parametrize('nonzero', (False, True)) +@pytest.mark.parametrize('shadow,delta', ((False, .000000001), (True, .01), (True, -.01))) +def test_actual_full_to_subset_prefix_matches_independent_tail_state_logs_maps_and_native(domain, nonzero, shadow, delta): + full, subset = full_and_subset(domain, nonzero=nonzero) + source = full._EventControllerReplay__controller_prefix + assert source is not None and len(source[1]) == 30 + independent = EventControllerReplay(subset.engine, subset.objective, subset.original_anchor, activation_plan=subset.activation_plan) + assert subset.reuse_subset_causal_features(full) is True + rebound = subset._EventControllerReplay__controller_prefix + assert rebound is not None and rebound[0] == subset._causal_input_key(.01) + assert rebound[0] != source[0] and rebound[1:] == source[1:] + assert all(type(part) is MappingProxyType and part is not old for part, old in zip(rebound[1:], source[1:], strict=True)) + assert subset._EventControllerReplay__strict_rollouts is None + assert subset._EventControllerReplay__identification_token is not full._EventControllerReplay__identification_token + with pytest.raises(TypeError): + rebound[1][0] = 1. + coordinates = (subset.reference[0] + delta,) + expected, before, native_before = observed(independent, coordinates, shadow=shadow) + actual, after, native_after = observed(subset, coordinates, shadow=shadow) + assert pickle.dumps(actual, protocol=5) == pickle.dumps(expected, protocol=5) + assert len(before) == 60 and len(after) == 30 and after == before[30:] + assert native_after == native_before + + +@pytest.mark.parametrize('defect', ('missing', 'stale', 'mutable', 'bad_raw_type', 'bad_snapshot', 'nondefault_epsilon')) +def test_missing_stale_unknown_prefix_does_not_change_valid_fd_transfer(defect): + epsilon = .02 if defect == 'nondefault_epsilon' else .01 + full, subset = full_and_subset(epsilon=epsilon) + source = full._EventControllerReplay__controller_prefix + if defect == 'missing': + full._EventControllerReplay__controller_prefix = None + elif defect == 'stale': + full._EventControllerReplay__controller_prefix = (b'not-current', *source[1:]) + elif defect == 'mutable': + full._EventControllerReplay__controller_prefix = (source[0], dict(source[1]), *source[2:]) + elif defect == 'bad_raw_type': + raw = dict(source[1]) + raw[next(iter(raw))] = 0 # Must not alias raw Float64 output. + full._EventControllerReplay__controller_prefix = (source[0], MappingProxyType(raw), *source[2:]) + elif defect == 'bad_snapshot': + snapshots = dict(source[3]) + snapshots[next(iter(snapshots))] = object() + full._EventControllerReplay__controller_prefix = (*source[:3], MappingProxyType(snapshots)) + assert subset.reuse_subset_causal_features(full, epsilon=epsilon) is True + assert subset._EventControllerReplay__controller_prefix is None + assert subset.projected and subset._EventControllerReplay__strict_rollouts is None + with mock.patch('openpilot.starpilot.flm.event_controller_replay._run', wraps=_run) as run: + subset._rollout((subset.reference[0] + .000000001,), + _identification_token=subset._EventControllerReplay__identification_token) + assert run.call_count == 60 + + +@pytest.mark.parametrize('change', ('prior', 'foreign_engine', 'plan', 'late_fault', 'native')) +def test_existing_fd_guard_failure_never_transfers_controller_certificate(change): + full, subset = full_and_subset() + if change == 'prior': + subset.prior_full['ff_gain_right'] = .002 + elif change == 'foreign_engine': + other, _ = full_and_subset() + subset.engine = other.engine + elif change == 'plan': + subset.activation_plan = replace(subset.activation_plan, physical_context='c' * 64) + elif change == 'late_fault': + full.records[-1] = replace(full.records[-1], healthy=False) + else: + stamp, event = next(iter(full.emissions.events.items())) + full.emissions.events[stamp] = replace(event, witness=event.witness._replace(final_raw=event.witness.final_raw + 1)) + assert subset.reuse_subset_causal_features(full) is False + assert subset._EventControllerReplay__controller_prefix is None + assert subset._EventControllerReplay__strict_rollouts is None + + +def test_source_shadow_token_cannot_become_destination_permission(): + full, subset = full_and_subset() + assert subset.reuse_subset_causal_features(full) + with pytest.raises(ValueError, match='identification scope'): + subset._rollout((.01,), _identification_token=full._EventControllerReplay__identification_token) diff --git a/openpilot/starpilot/flm/tests/test_fit_worker_gc.py b/openpilot/starpilot/flm/tests/test_fit_worker_gc.py new file mode 100644 index 0000000000..e0873fcad3 --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_fit_worker_gc.py @@ -0,0 +1,103 @@ +"""One-shot worker lifecycle tests, not fabricated replay qualification.""" +import gc +import io +import json +import sys +from types import SimpleNamespace + +import pytest + +from openpilot.starpilot.flm import fit_worker + + +@pytest.fixture +def worker_io(monkeypatch): + prior = gc.isenabled() + incoming, outgoing = io.BytesIO(b'{"test":true}'), io.BytesIO() + monkeypatch.setattr(fit_worker, 'sys', SimpleNamespace(platform='darwin', + stdin=SimpleNamespace(buffer=incoming), stdout=SimpleNamespace(buffer=outgoing))) + monkeypatch.setattr(fit_worker.os, 'nice', lambda _: None) + monkeypatch.setattr(fit_worker.os, 'sched_setscheduler', lambda *args: None, raising=False) + limits = [] + monkeypatch.setattr(fit_worker.resource, 'getrlimit', lambda kind: (-1, -1)) + monkeypatch.setattr(fit_worker.resource, 'setrlimit', lambda *args: limits.append(args)) + yield incoming, outgoing, limits + if prior: + gc.enable() + else: + gc.disable() + + +def install_fit(monkeypatch, call): + monkeypatch.setitem(sys.modules, 'openpilot.starpilot.flm.learner_factory', SimpleNamespace(fit_snapshot=call)) + + +@pytest.mark.parametrize('enabled', (True, False)) +def test_success_restores_exact_gc_state_and_original_bounds(monkeypatch, worker_io, enabled): + incoming, outgoing, limits = worker_io + (gc.enable if enabled else gc.disable)() + calls = [] + + def fit(snapshot): + calls.append(snapshot) + assert not gc.isenabled() + # Even a helper toggling GC cannot alter the caller's final state. + gc.enable() + return {'status': 'collecting'} + + original_dumps = json.dumps + + def encode(*args, **kwargs): + assert gc.isenabled() is enabled + return original_dumps(*args, **kwargs) + + install_fit(monkeypatch, fit) + monkeypatch.setattr(fit_worker.json, 'dumps', encode) + assert fit_worker.main() == 0 + assert gc.isenabled() is enabled + assert calls == [{'test': True}] + assert outgoing.getvalue() == b'{"status":"collecting"}' + assert limits == [(fit_worker.resource.RLIMIT_AS, (2 * 1024**3, -1)), + (fit_worker.resource.RLIMIT_CPU, (20, -1))] + assert fit_worker.MAX_INPUT == 8 * 1024**2 and fit_worker.MAX_OUTPUT == 65536 + assert incoming.tell() == len(b'{"test":true}') + + +@pytest.mark.parametrize('enabled', (True, False)) +@pytest.mark.parametrize('failure', (ValueError, MemoryError)) +def test_fit_failure_restores_prior_gc_and_emits_nothing(monkeypatch, worker_io, enabled, failure): + _, outgoing, _ = worker_io + (gc.enable if enabled else gc.disable)() + + def fit(_): + assert not gc.isenabled() + raise failure('optional fit failure') + + install_fit(monkeypatch, fit) + assert fit_worker.main() == 1 + assert gc.isenabled() is enabled + assert outgoing.getvalue() == b'' + + +def test_oversized_input_never_enters_fit_or_changes_gc(monkeypatch, worker_io): + incoming, outgoing, _ = worker_io + gc.enable() + incoming.seek(0) + incoming.write(b'x' * (fit_worker.MAX_INPUT + 1)) + incoming.seek(0) + install_fit(monkeypatch, lambda _: pytest.fail('oversized input reached fitter')) + assert fit_worker.main() == 1 + assert gc.isenabled() and outgoing.getvalue() == b'' + + +def test_output_bound_remains_after_gc_restoration(monkeypatch, worker_io): + _, outgoing, _ = worker_io + gc.enable() + + def fit(_): + assert not gc.isenabled() + return {'data': 'x' * fit_worker.MAX_OUTPUT} + + install_fit(monkeypatch, fit) + assert fit_worker.main() == 1 + assert gc.isenabled() and outgoing.getvalue() == b'' diff --git a/openpilot/starpilot/flm/tests/test_prepared_car_state.py b/openpilot/starpilot/flm/tests/test_prepared_car_state.py new file mode 100644 index 0000000000..32b55ad710 --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_prepared_car_state.py @@ -0,0 +1,104 @@ +"""Actual selected-controller reader equivalence; no fit/application authority.""" +import math +from types import SimpleNamespace +from unittest import mock + +import pytest + +from opendbc.car import structs +from opendbc.car.car_helpers import interfaces +from opendbc.car.hyundai.values import CAR as HYUNDAI +from openpilot.starpilot.flm.controller_replay import ( + SelectedControllerFactory, _PreparedCarStates, _car_state, _replay_car_state, _run, capture_snapshot, frame_input, +) +from openpilot.starpilot.flm.tests.test_event_replay_cost import engine_for +from openpilot.starpilot.flm.torque_adapter import Objective + + +def inputs_for(vm, tick=0): + cs = structs.CarState.new_message(vEgo=23.123456789, steeringAngleDeg=.1 * math.sin(tick / 20), + steeringRateDeg=.00003, gearShifter='drive') + values = frame_input(cs, vm, SimpleNamespace(angleOffsetDeg=.02, roll=.001), active=True, + safety_limited=False, desired_curvature=.0001 * math.cos(tick / 60), curvature_limited=False, lat_delay=.2) + return values + (('control_frame', float(tick + 1)), ('raw_model_curvature', .0001)) + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +def test_full_selected_updates_and_every_captured_state_match_fresh_builder(domain): + if domain == 'starpilot': + fingerprint = HYUNDAI.HYUNDAI_IONIQ_6 + cp = interfaces[fingerprint].get_non_essential_params(fingerprint) + factory = SelectedControllerFactory(cp, 'starpilot') + else: + factory = engine_for(domain).factory + actual, vm = factory.create() + cached, cached_vm = factory.create() + cache = _PreparedCarStates() + objective = Objective('feedforward', 'high', 'steady') + original_cp = factory.cp.as_builder().to_bytes() + for tick in range(220): + inputs = inputs_for(vm, tick) + prepared = cache.get(inputs) + assert prepared is not None and prepared.inputs is inputs + expected, original_log = _run(actual, vm, inputs, objective, (0., 0.)) + observed, cached_log = _run(cached, cached_vm, inputs, objective, (0., 0.), _prepared_car_state=prepared) + assert observed == expected and cached_log.to_dict() == original_log.to_dict() + assert capture_snapshot(cached, cached_vm) == capture_snapshot(actual, vm) + assert cache.get(inputs) is prepared + assert prepared.reader.as_builder().to_bytes() == _car_state(dict(inputs)).to_bytes() + assert factory.cp.as_builder().to_bytes() == original_cp + + +def test_exact_capnp_fields_enum_float32_reader_immutability_and_replacement(): + _, vm = engine_for('torque').factory.create() + inputs = inputs_for(vm) + cache = _PreparedCarStates() + prepared = cache.get(inputs) + fresh = _car_state(dict(inputs)) + assert prepared.reader.to_dict() == fresh.to_dict() + assert prepared.reader.as_builder().to_bytes() == fresh.to_bytes() + assert prepared.reader.total_size.word_count <= 32 + assert prepared.reader.vEgo == fresh.vEgo + assert prepared.reader.gearShifter == fresh.gearShifter + assert prepared.reader.total_size.word_count == fresh.total_size.word_count + with pytest.raises(AttributeError): + prepared.reader.vEgo = 0. + replaced = (*inputs[:-1], ('raw_model_curvature', -.0001)) + assert cache.get(replaced) is not prepared + assert _replay_car_state(replaced, dict(replaced), prepared).to_dict() == _car_state(dict(replaced)).to_dict() + + +@pytest.mark.parametrize('value', ([], [('vEgo', 20.)], (['vEgo', 20.],), (('vEgo', []),))) +def test_mutable_or_unknown_input_shapes_take_original_path(value): + assert _PreparedCarStates().get(value) is None + + +def test_subclasses_fall_back_and_duplicate_key_semantics_are_unchanged(): + class Scalar(float): + pass + _, vm = engine_for('torque').factory.create() + inputs = inputs_for(vm) + cache = _PreparedCarStates() + assert cache.get((*inputs, ('extra', Scalar(1.)))) is None + duplicate = (*inputs, ('vEgo', 21.125)) + assert cache.get(duplicate).reader.vEgo == _car_state(dict(duplicate)).vEgo == 21.125 + + +def test_cache_is_strongly_retained_and_bounded_to_twelve_thousand_inputs(): + cache = _PreparedCarStates() + with mock.patch('openpilot.starpilot.flm.controller_replay._car_state') as build: + for tick in range(12000): + inputs = (('tick', tick),) + assert cache.get(inputs).inputs is inputs + assert cache.get((('tick', 12000),)) is None + assert build.call_count == 12000 + assert len(cache._PreparedCarStates__entries) == 12000 + + +def test_default_call_still_constructs_fresh_car_state_every_time(): + _, vm = engine_for('torque').factory.create() + inputs = inputs_for(vm) + with mock.patch('openpilot.starpilot.flm.controller_replay._car_state', wraps=_car_state) as build: + a = _replay_car_state(inputs, dict(inputs), None) + b = _replay_car_state(inputs, dict(inputs), None) + assert build.call_count == 2 and a is not b and a.to_dict() == b.to_dict() diff --git a/openpilot/starpilot/flm/tests/test_production_event_contract.py b/openpilot/starpilot/flm/tests/test_production_event_contract.py index 8717bb1f8f..06d595b352 100644 --- a/openpilot/starpilot/flm/tests/test_production_event_contract.py +++ b/openpilot/starpilot/flm/tests/test_production_event_contract.py @@ -8,7 +8,7 @@ from opendbc.car.car_helpers import interfaces from opendbc.car.honda.values import CAR as HONDA from opendbc.car.hyundai.values import CAR as HKG from opendbc.car.toyota.values import CAR as TOYOTA -from openpilot.starpilot.flm.clipping_quality import RECORDED_DEBT, STATE_ALIGNED_DEBT, QUALIFIED_EVENT_CLIPPING_QUALITY, event_clipping_quality +from openpilot.starpilot.flm.clipping_quality import RECORDED_DEBT, STATE_ALIGNED_DEBT, ZERO_REFERENCE_QUALITY, event_clipping_quality from openpilot.starpilot.flm.event_fit_factory import fit_event_snapshot from openpilot.starpilot.flm.event_objective import EffectObjective from openpilot.starpilot.flm.learner_factory import from_evidence @@ -62,10 +62,10 @@ def test_actual_live_factory_adopts_both_domains_without_changing_legacy_default old = from_evidence(controls.CP, evidence, key, source=source) assert live is not None and old is not None assert controls.global_learning.descriptor.domain == domain - assert live.replay_version == 3 and live.clipping_quality == QUALIFIED_EVENT_CLIPPING_QUALITY + assert live.replay_version == 3 and live.clipping_quality == ZERO_REFERENCE_QUALITY assert old.replay_version == 2 and old.clipping_quality == RECORDED_DEBT live.select_objective(('event', 'deadband.high')) - assert live.replay_version == 3 and live.clipping_quality == QUALIFIED_EVENT_CLIPPING_QUALITY + assert live.replay_version == 3 and live.clipping_quality == ZERO_REFERENCE_QUALITY assert production_adapter_for_evidence(controls.CP, evidence, key, source=None) is None assert production_adapter_for_evidence(controls.CP, evidence, key, source=replace(source, enabled=False)) is None assert production_adapter_for_evidence(controls.CP, evidence, key, source=replace(source, manual=True)) is None diff --git a/openpilot/starpilot/flm/tests/test_recorded_cp_digest.py b/openpilot/starpilot/flm/tests/test_recorded_cp_digest.py new file mode 100644 index 0000000000..e1d5e80e9d --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_recorded_cp_digest.py @@ -0,0 +1,179 @@ +"""Replay-local canonical CP reuse rereads native bytes; no live/global authority.""" +from dataclasses import replace +from types import SimpleNamespace +from unittest import mock + +import pytest + +from opendbc.car import structs +from openpilot.starpilot.flm import runtime_adapter_registry as registry +from openpilot.starpilot.flm.tests.test_event_controller_prefix import fixture + + +def changed(engine, update): + cp = engine.factory.cp.as_builder() + update(cp) + engine.factory.cp = cp.as_reader() + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +def test_actual_owned_cp_hits_only_one_entry_and_every_lookup_reads_bytes(domain): + engine = fixture(domain).engine + original = registry.car_params_digest + with mock.patch.object(registry, 'car_params_digest', wraps=original) as canonical: + assert engine._recorded_cp_digest() == original(engine.factory.cp) + saved = engine.__dict__['_ControllerReplay__recorded_cp_digest_cache'] + for _ in range(5): + assert engine._recorded_cp_digest() == saved[1] + assert canonical.call_count == 1 + changed(engine, lambda cp: setattr(cp, 'carVin', '1HGBH41JXMN109186')) + assert engine._recorded_cp_digest() == original(engine.factory.cp) + assert canonical.call_count == 2 + assert engine.__dict__['_ControllerReplay__recorded_cp_digest_cache'][0] != saved[0] + + +@pytest.mark.parametrize('field,value', (('mass', 0.), ('mass', -0.), ('steerLimitTimer', float('inf')), + ('steerLimitTimer', -float('inf')), ('carVin', 'new-source'))) +def test_actual_scalar_bits_infinity_and_source_changes_match_original(field, value): + engine = fixture().engine + before = engine._recorded_cp_digest() + changed(engine, lambda cp: setattr(cp, field, value)) + assert engine._recorded_cp_digest() == registry.car_params_digest(engine.factory.cp) + assert engine._recorded_cp_digest() != before + if field == 'mass': + plus = engine._recorded_cp_digest() + changed(engine, lambda cp: setattr(cp, 'mass', -value)) + # Test both directions of native Float32 signed zero identity. + assert engine._recorded_cp_digest() != plus + + +def test_actual_safety_config_native_bits_are_not_hidden_by_cache(): + engine = fixture().engine + before = engine._recorded_cp_digest() + cp = engine.factory.cp.as_builder() + assert len(cp.safetyConfigs) + cp.safetyConfigs[0].safetyParam ^= 1 + engine.factory.cp = cp.as_reader() + assert engine._recorded_cp_digest() == registry.car_params_digest(engine.factory.cp) + assert engine._recorded_cp_digest() != before + + +@pytest.mark.parametrize('wrong', ('schema', 'object', 'factory')) +def test_unknown_or_wrong_schema_keeps_original_digest_each_time(wrong): + engine = fixture().engine + if wrong == 'schema': + engine.factory.cp = structs.CarState.new_message().as_reader() + elif wrong == 'object': + engine.factory.cp = SimpleNamespace(to_dict=lambda: {'negativeZero': -0., 'sentinel': float('inf')}) + else: + engine.factory = SimpleNamespace(cp=engine.factory.cp) + original = registry.car_params_digest + with mock.patch.object(registry, 'car_params_digest', wraps=original) as canonical: + expected = original(engine.factory.cp) + assert engine._recorded_cp_digest() == engine._recorded_cp_digest() == expected + assert canonical.call_count == 2 + assert '_ControllerReplay__recorded_cp_digest_cache' not in engine.__dict__ + + +@pytest.mark.parametrize('failure', ('serialization', 'changed_during_digest', 'canonical_error')) +def test_serialization_failure_and_unstable_pair_never_cache_errors_or_retry_original(failure): + engine = fixture().engine + original = registry.car_params_digest + real = engine.factory.cp + class Reader: + schema = structs.CarParams.schema + def __init__(self): + self.calls = 0 + def to_dict(self): + return real.to_dict() + def as_builder(self): + self.calls += 1 + if failure == 'serialization': + raise ValueError('serializer unavailable') + raw = b'before' if self.calls == 1 else b'after' + return SimpleNamespace(to_bytes=lambda: raw) + engine.factory.cp = Reader() + with mock.patch('capnp.lib.capnp._DynamicStructReader', Reader): + if failure == 'canonical_error': + with mock.patch.object(registry, 'car_params_digest', side_effect=ValueError('original digest error')) as canonical: + with pytest.raises(ValueError, match='original digest error'): + engine._recorded_cp_digest() + assert canonical.call_count == 1 + else: + with mock.patch.object(registry, 'car_params_digest', wraps=original) as canonical: + assert engine._recorded_cp_digest() == original(real) + assert canonical.call_count == 1 + assert '_ControllerReplay__recorded_cp_digest_cache' not in engine.__dict__ + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +def test_actual_normalize_trace_cp_binding_uses_cache_without_losing_any_recent_states(domain): + engine = fixture(domain).engine + cp_digest = registry.car_params_digest(engine.factory.cp) + frames = tuple(engine.records) + engine.records.clear() + original = registry.car_params_digest + with mock.patch.object(registry, 'car_params_digest', wraps=original) as canonical: + for frame in frames: + trace = frame.effect_trace + trace = replace(trace, envelope=replace(trace.envelope, cp_sha256=cp_digest).validate()).validate() + assert engine.normalize(replace(frame, effect_trace=trace), _verify_only=True) is not None + assert canonical.call_count == 1 + assert len(engine.recent_states) == len(frames) == len(engine.records) == 60 + assert not engine.failed + + +def test_mutation_through_builder_changes_same_reader_identity_and_recomputes(): + engine = fixture().engine + builder = engine.factory.cp.as_builder() + reader = builder.as_reader() + engine.factory.cp = reader + before = engine._recorded_cp_digest() + builder.carVin = 'same-reader-new-native-bytes' + assert engine.factory.cp is reader + assert engine._recorded_cp_digest() == registry.car_params_digest(reader) + assert engine._recorded_cp_digest() != before + + +def test_same_schema_node_id_without_full_schema_equality_does_not_cache(): + engine = fixture().engine + real = engine.factory.cp + class Schema: + node = SimpleNamespace(id=structs.CarParams.schema.node.id) + def __eq__(self, other): + return False + class Reader: + schema = Schema() + def to_dict(self): + return real.to_dict() + def as_builder(self): + raise AssertionError('wrong schema must not be serialized for a cache hit') + engine.factory.cp = Reader() + original = registry.car_params_digest + with mock.patch('capnp.lib.capnp._DynamicStructReader', Reader), \ + mock.patch.object(registry, 'car_params_digest', wraps=original) as canonical: + assert engine._recorded_cp_digest() == engine._recorded_cp_digest() == original(real) + assert canonical.call_count == 2 + assert '_ControllerReplay__recorded_cp_digest_cache' not in engine.__dict__ + + +def test_malformed_original_result_is_not_cached_as_a_success(): + engine = fixture().engine + with mock.patch.object(registry, 'car_params_digest', return_value=None) as canonical: + assert engine._recorded_cp_digest() is None and engine._recorded_cp_digest() is None + assert canonical.call_count == 2 + assert '_ControllerReplay__recorded_cp_digest_cache' not in engine.__dict__ + + +def test_replacing_canonical_function_invalidates_cache_without_any_data_identity_shortcut(): + engine = fixture().engine + original = registry.car_params_digest + assert engine._recorded_cp_digest() == original(engine.factory.cp) + with mock.patch.object(registry, 'car_params_digest', wraps=original) as replacement: + assert engine._recorded_cp_digest() == original(engine.factory.cp) + assert engine._recorded_cp_digest() == original(engine.factory.cp) + assert replacement.call_count == 1 + saved = engine.__dict__['_ControllerReplay__recorded_cp_digest_cache'] + assert saved[2] is replacement + engine._recorded_cp_digest() + assert engine.__dict__['_ControllerReplay__recorded_cp_digest_cache'][2] is original diff --git a/openpilot/starpilot/flm/tests/test_snapshot_scalar_fastpath.py b/openpilot/starpilot/flm/tests/test_snapshot_scalar_fastpath.py new file mode 100644 index 0000000000..68624047c8 --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_snapshot_scalar_fastpath.py @@ -0,0 +1,120 @@ +"""Exact leaf speedup equivalence, including all original generic fallbacks.""" +from collections import deque +import math +import pickle +from unittest import mock + +import numpy as np +import pytest + +from openpilot.starpilot.flm import controller_replay as replay +from openpilot.starpilot.flm.selected_effects import EffectReplayRecordedSource +from openpilot.starpilot.flm.tests.test_event_controller_prefix import fixture + + +def original_tree(values, path, value): + # Original complete recursive traversal, without calling the proposed tree. + if type(value).__module__.startswith('capnp') and 'List' in type(value).__name__: + value = list(value) + if isinstance(value, (tuple, list, deque)): + values.append((path + '.kind', float(3 if isinstance(value, deque) else 2 if isinstance(value, tuple) else 1))) + values.append((path + '.length', float(len(value)))) + if isinstance(value, deque): + values.append((path + '.maxlen', float(value.maxlen or 0))) + for i, item in enumerate(value): + original_tree(values, path + '.' + str(i), item) + else: + values.append((path, replay._numeric(value))) + if len(values) > replay.MAX_SNAPSHOT_VALUES: + raise ValueError('snapshot exceeds bounded primitive budget') + + +def outcome(function, value, initial=()): + values = list(initial) + error = None + try: + function(values, 'state', value) + except (TypeError, ValueError, OverflowError, AttributeError) as exc: + error = (type(exc), str(exc)) + return pickle.dumps(values, protocol=5), error + + +@pytest.mark.parametrize('value', (0., -0., .12345678901234567, 1, True, False, -(2 ** 100), 2 ** 10000, + float('nan'), float('inf'), -float('inf'), 'invalid', None, + (1., [False, -0., .3]), deque([1., 2., (3.,)], maxlen=6), + np.float64(.123), np.float32(-.5), np.int64(3), np.bool_(True), np.float64('nan'))) +@pytest.mark.parametrize('size', (0, 1023, 1024)) +def test_every_leaf_fallback_order_floatbits_error_and_max_bound_match_original(value, size): + initial = tuple((str(i), 1.) for i in range(size)) + assert outcome(replay._tree, value, initial) == outcome(original_tree, value, initial) + + +def test_subclass_coercion_and_native_capnp_pid_list_stay_original_fallback(): + class Float(float): + calls = 0 + def __float__(self): + type(self).calls += 1 + return .25 + value = Float(1.) + expected = outcome(original_tree, value) + assert Float.calls == 1 + Float.calls = 0 + assert outcome(replay._tree, value) == expected and Float.calls == 1 + class Tuple(tuple): + pass + assert outcome(replay._tree, Tuple((.1, -0.))) == outcome(original_tree, Tuple((.1, -0.))) + from opendbc.car import structs + cp = structs.CarParams.new_message() + pid = cp.lateralTuning.init('pid') + pid.kpBP = [0., 5., 20.] + assert outcome(replay._tree, pid.kpBP) == outcome(original_tree, pid.kpBP) + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot', 'pid')) +def test_actual_warm_selected_snapshot_every_value_and_hidden_invalid_state_match_original(domain): + engine = fixture(domain, nonzero=True) + controller, vm = engine.factory.create() + replay.restore_snapshot(controller, vm, engine.checkpoints[0][1]) + for frame in engine.records: + value, _ = replay._run(controller, vm, frame.inputs, engine.engine.objective, (0., 0.), frame.torque_values, + actual_stage_residual=frame.actual_stage_residual, effect_trial=EffectReplayRecordedSource(frame.effect_trace)) + assert replay.same_float(value, frame.actual_lac_command) + proposed = replay.capture_snapshot(controller, vm) + with mock.patch.object(replay, '_tree', original_tree): + before = replay.capture_snapshot(controller, vm) + assert pickle.dumps(proposed, protocol=5) == pickle.dumps(before, protocol=5) + controller.pid.i = math.nan + for function in (replay._tree, original_tree): + with mock.patch.object(replay, '_tree', function), pytest.raises(ValueError, match='finite numeric'): + replay.capture_snapshot(controller, vm) + + +@pytest.mark.parametrize('domain', ('torque', 'starpilot')) +def test_actual_manual_surface_and_duplicate_paths_guards_are_unchanged(domain): + engine = fixture(domain) + controller, vm = engine.factory.create() + controller.flm_surface = object() + for function in (replay._tree, original_tree): + with mock.patch.object(replay, '_tree', function), pytest.raises(ValueError, match='manual surface'): + replay.capture_snapshot(controller, vm) + controller.flm_surface = None + controller.pid.i = None + setattr(controller.pid, 'i.none', 1.) + for function in (replay._tree, original_tree): + with mock.patch.object(replay, '_tree', function), pytest.raises(ValueError, match='cardinality'): + replay.capture_snapshot(controller, vm) + + +def test_custom_metaclass_equality_cannot_impersonate_exact_builtin_leaf(): + class EqualFloat(type): + def __eq__(cls, other): + return other is float + __hash__ = type.__hash__ + class NotNumeric(metaclass=EqualFloat): + def __float__(self): + return .25 + value = NotNumeric() + assert type(value) == float # noqa: E721 - exercise adversarial class equality, not type identity. + assert type(value) is not float + assert outcome(replay._tree, value) == outcome(original_tree, value) + assert outcome(replay._tree, value)[1] == (ValueError, 'snapshot requires finite numeric state') diff --git a/openpilot/starpilot/flm/tests/test_zero_reference_quality.py b/openpilot/starpilot/flm/tests/test_zero_reference_quality.py new file mode 100644 index 0000000000..5a0d24f28e --- /dev/null +++ b/openpilot/starpilot/flm/tests/test_zero_reference_quality.py @@ -0,0 +1,184 @@ +"""Exact native zero-reference boundary; no fit, native authority or plant result.""" +from dataclasses import asdict, replace +from unittest import mock + +import pytest +from opendbc.car import DT_CTRL, rate_limit +from opendbc.car.gm.values import CAR as GM +from opendbc.car.honda.values import CAR as HONDA +from opendbc.car.toyota.values import CAR as TOYOTA +from opendbc.car.steering_projection import normalized_applied_command, _shaped_input, float32 +from openpilot.starpilot.flm.clipping_quality import ( + ZERO_REFERENCE_QUALITY, EXACT_EMISSION_QUALITY, QUALIFIED_EVENT_CLIPPING_QUALITY, + NATIVE_SHADOW_IDENTIFICATION, FIXED_MAP_IDENTIFICATION, RESTORE_ZERO_REFERENCE_VERSION, + RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION, + zero_reference_relief_is_admitted, native_relief_is_admitted, event_clipping_quality, restore_policy, restore_quality, +) +from openpilot.starpilot.flm.event_activation import ( + ZERO_REFERENCE_ACTIVATION_VERSION, bind_activation_model, activation_matches_model, activation_model_from_dict, model_clipping_quality, +) +from openpilot.starpilot.flm.restore_activation import restore_plan_from_dict +from openpilot.starpilot.flm.torque_emission_replay import EncoderBranch +from openpilot.starpilot.flm.tests.test_event_activation import fitted_model, primitive_plan +from openpilot.starpilot.flm.tests.test_state_aligned_encoder import captured_event +from openpilot.starpilot.flm.tests.test_steering_projection import make_interface, control, apply, settle, install_observer +from openpilot.starpilot.flm.tests.test_declared_restore import envelope, plan_for, payload + + +def test_exact_actual_v9_integer_witness_and_all_sign_mirrors(): + values = (-86, -86, -85, -87, -86, -86, 1, 0, 1) + for sign in (-1, 1): + mirrored = tuple(sign * value for value in values) + assert not native_relief_is_admitted(*mirrored) + assert zero_reference_relief_is_admitted(*mirrored) + # This admits no nonzero reference debt and no generalized <= comparator. + changes = ((6, 0), (7, 1), (7, -1), (8, 0), (2, -86), (2, -87), + (2, 0), (2, 1), (4, -85), (5, -85), (5, -87), (3, -85), + (4, 0), (6, -1), (8, -1), (0, True), (2, float('nan')), (5, float('inf'))) + for index, value in changes: + changed = list(values) + changed[index] = value + assert not zero_reference_relief_is_admitted(*changed), (index, value) + + +def test_partial_order_boundary_property_has_no_tolerance_or_zero_generalization(): + for original in range(3, 15): + for reduction in range(1, original): + for sign in (-1, 1): + args = tuple(sign * v for v in (original, original, original - reduction, + original + 2, original, original, -2, 0, -reduction)) + assert zero_reference_relief_is_admitted(*args) + for index in range(9): + changed = list(args) + if index == 7: + changed[index] = sign * 1e-12 + else: + changed[index] = 0 + assert not zero_reference_relief_is_admitted(*changed) + + +def unwind(platform): + ci = make_interface(platform) + assert install_observer(ci) + settle(ci, control(.1)) + for _ in range(4): + _, _, witness = apply(ci, control(.01)) + if witness is not None: + break + assert witness is not None and 1 < witness.unlimited_raw < witness.final_raw + return replace(captured_event(platform), witness=witness, limited=True) + + +@pytest.mark.parametrize('platform', (TOYOTA.TOYOTA_COROLLA_TSS2, GM.GMC_ACADIA)) +def test_registered_native_boundary_old_rejects_new_exact_reference_admits(platform): + event = unwind(platform) + witness = event.witness + candidate = (witness.unlimited_raw - 1) / witness.effective_max + previous = witness.unlimited_raw + dict(witness.limits)['STEER_DELTA_DOWN'] + old = EncoderBranch(quality_contract=EXACT_EMISSION_QUALITY) + old.previous = previous + with pytest.raises(ValueError, match='clipping regression'): + old.step(event, candidate) + branch = EncoderBranch(quality_contract=ZERO_REFERENCE_QUALITY) + branch.previous = previous + delivered = branch.step(event, candidate) + assert branch.previous == witness.unlimited_raw + assert delivered == normalized_applied_command(witness._replace(final_raw=witness.unlimited_raw)) + assert 0 < branch.previous < witness.final_raw + # Future frame retains strict original-unclipped and own-state native checks. + future = replace(event, control_ns=event.control_ns + 100_000_000, + state_ns=event.state_ns + 100_000_000, apply_ns=event.apply_ns + 100_000_000, limited=False) + with pytest.raises(ValueError): + branch.step(future, 1.) + for changed in (replace(event, limited=False), replace(event, witness=witness._replace(steer_requested=False)), + replace(event, witness=witness._replace(codec_id='volkswagen.HCA_01'))): + denied = EncoderBranch(quality_contract=ZERO_REFERENCE_QUALITY) + denied.previous = previous + with pytest.raises(ValueError): + denied.step(changed, candidate) + + +@pytest.mark.parametrize('modified', (False, True)) +def test_honda_zero_reference_remains_in_shaped_normalized_and_decoded_domain(modified): + platform = HONDA.HONDA_CIVIC_BOSCH if modified else HONDA.HONDA_CIVIC + # Original production witness is a real normalized-rate unwind, inverse CAN sign. + from opendbc.car import structs + firmware = (structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.eps, address=0x18DA30F1, subAddress=0, + fwVersion=b'39990-TGG,A020\x00\x00', brand='honda'),) if modified else () + ci = make_interface(platform, firmware) + assert install_observer(ci) + settle(ci, control(.8)) + for _ in range(4): + _, _, witness = apply(ci, control(.01)) + if witness is not None: + break + event = replace(captured_event(platform, modified=modified), witness=witness, limited=True) + shape = tuple(witness.shape) if witness.shape else () + reference = _shaped_input(witness, float32(witness.requested_torque), shape) + down = dict(witness.limits)['STEER_DELTA_DOWN'] * DT_CTRL + previous = reference + down + assert rate_limit(reference, previous, -down, dict(witness.limits)['STEER_DELTA_UP'] * DT_CTRL) == reference + # Candidate shaping and the full actual native function remain unstubbed. + old = EncoderBranch(quality_contract=EXACT_EMISSION_QUALITY) + old.previous = previous + with pytest.raises(ValueError, match='clipping regression'): + old.step(event, .005) + branch = EncoderBranch(quality_contract=ZERO_REFERENCE_QUALITY) + branch.previous = previous + delivered = branch.step(event, .005) + assert 0 < delivered <= normalized_applied_command(witness) + assert branch.previous == reference and witness.final_raw < 0 + if modified: + assert branch.civic is not None and branch.civic != tuple(witness.shape[:4]) + # Both original-paired and candidate override decisions still run before quality. + if modified: + pressed = replace(event, witness=witness._replace(shape=tuple(witness.shape[:4]) + (18., True, -1.))) + veto = EncoderBranch(quality_contract=ZERO_REFERENCE_QUALITY) + veto.previous = previous + veto.civic = (1., -.2, .095, False) + with pytest.raises(ValueError, match='driver override'): + veto.step(pressed, .005) + + +def test_new_policy_trial_restore_model_proof_and_old_restore_mappings_are_distinct(): + assert QUALIFIED_EVENT_CLIPPING_QUALITY == EXACT_EMISSION_QUALITY + assert tuple(restore_policy(v) for v in (4, 5, 6)) == ( + RESTORE_ACTIVATION_VERSION, RESTORE_AGGREGATE_ACTIVATION_VERSION, RESTORE_ZERO_REFERENCE_VERSION) + assert restore_quality(4) == restore_quality(5) == EXACT_EMISSION_QUALITY + assert restore_quality(6) == ZERO_REFERENCE_QUALITY + row = {'event_version': 3, 'event_operation': 'trial', 'event_clipping_quality': ZERO_REFERENCE_QUALITY, + 'event_identification_policy': NATIVE_SHADOW_IDENTIFICATION} + assert event_clipping_quality(row) == ZERO_REFERENCE_QUALITY + trial = replace(primitive_plan(fitted_model()), version=ZERO_REFERENCE_ACTIVATION_VERSION).validate() + assert trial.clipping_quality == ZERO_REFERENCE_QUALITY + bound = bind_activation_model(fitted_model(), trial) + assert model_clipping_quality(bound) == ZERO_REFERENCE_QUALITY + assert activation_matches_model(trial, activation_model_from_dict(asdict(bound))) + restore = replace(plan_for(envelope()), version=RESTORE_ZERO_REFERENCE_VERSION).validate() + assert restore.identification_policy == FIXED_MAP_IDENTIFICATION and restore.clipping_quality == ZERO_REFERENCE_QUALITY + model = bind_activation_model(fitted_model(), restore) + assert model_clipping_quality(model) == ZERO_REFERENCE_QUALITY + assert activation_matches_model(restore, activation_model_from_dict(asdict(model))) + assert not activation_matches_model(replace(restore, version=RESTORE_AGGREGATE_ACTIVATION_VERSION), model) + assert restore_plan_from_dict(asdict(restore)) == restore + row = {**payload(), 'event_version': 6, 'event_restore_policy': RESTORE_ZERO_REFERENCE_VERSION, + 'event_clipping_quality': ZERO_REFERENCE_QUALITY} + assert event_clipping_quality(row) == ZERO_REFERENCE_QUALITY + for change in ({'event_version': 5}, {'event_restore_policy': RESTORE_AGGREGATE_ACTIVATION_VERSION}, + {'event_clipping_quality': EXACT_EMISSION_QUALITY}, {'event_operation': 'trial'}, + {'event_identification_policy': NATIVE_SHADOW_IDENTIFICATION}): + with pytest.raises(ValueError): + event_clipping_quality({**row, **change}) + for name in ('event_restore_policy', 'event_clipping_quality', 'event_identification_policy'): + with pytest.raises(ValueError): + event_clipping_quality({k: v for k, v in row.items() if k != name}) + + +def test_actual_outer_factory_v6_dispatch_and_parent_wrong_version_rejections(): + from openpilot.starpilot.flm.tests import test_declared_restore as existing + row = {**existing.payload(), 'event_version': 6, 'event_restore_policy': RESTORE_ZERO_REFERENCE_VERSION, + 'event_clipping_quality': ZERO_REFERENCE_QUALITY} + with mock.patch.object(existing, 'payload', return_value=row): + existing.test_actual_child_and_outer_factory_dispatch_restore_v4_without_fd() + for configured, reported in ((6, 5), (5, 6), (6, 4), (4, 6)): + existing.test_actual_parent_rejects_restore_report_from_other_configured_version(configured, reported) diff --git a/openpilot/starpilot/flm/torque_emission_replay.py b/openpilot/starpilot/flm/torque_emission_replay.py index 0f1730a87c..99d5bfd241 100644 --- a/openpilot/starpilot/flm/torque_emission_replay.py +++ b/openpilot/starpilot/flm/torque_emission_replay.py @@ -11,7 +11,8 @@ import math from openpilot.starpilot.flm.torque_eligibility import bounded_context_output from openpilot.starpilot.flm.clipping_quality import ( - RECORDED_DEBT, STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, clipping_quality, debt_is_admitted, native_relief_is_admitted, + RECORDED_DEBT, STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY, + clipping_quality, debt_is_admitted, native_relief_is_admitted, zero_reference_relief_is_admitted, STRICT_IDENTIFICATION, NATIVE_SHADOW_IDENTIFICATION, identification_policy as validate_identification_policy, ) @@ -102,7 +103,8 @@ class EncoderBranch: def __init__(self, *, quality_contract=RECORDED_DEBT, identification_policy=STRICT_IDENTIFICATION): self.quality_contract = clipping_quality(quality_contract) self.identification_policy = validate_identification_policy(identification_policy) - if self.identification_policy == NATIVE_SHADOW_IDENTIFICATION and self.quality_contract not in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY): + if (self.identification_policy == NATIVE_SHADOW_IDENTIFICATION and + self.quality_contract not in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY)): raise ValueError('shadow identification requires combined quality proof') self.previous = None self.civic = None @@ -173,7 +175,7 @@ class EncoderBranch: candidate_demand = source - honda_limited_source raw = int(np.interp(-honda_limited_source * witness.effective_max, witness.lookup_x, witness.lookup_y)) admitted = debt_is_admitted(original_demand, reference_demand, candidate_demand, self.quality_contract) - if not admitted and self.quality_contract in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY) and not shadow and event.limited: + if not admitted and self.quality_contract in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY) and not shadow and event.limited: reference_raw = int(np.interp(-reference_limited * witness.effective_max, witness.lookup_x, witness.lookup_y)) recorded_output = normalized_applied_command(witness) reference_output = normalized_applied_command(witness._replace(final_raw=reference_raw)) @@ -184,7 +186,13 @@ class EncoderBranch: admitted = (requested * witness.requested_torque > 0 and abs(requested) < abs(witness.requested_torque) and native_relief_is_admitted(witness.codec_input_torque, reference_source, source, recorded_output, reference_output, candidate_output, original_demand, reference_demand, candidate_demand)) - if not admitted and self.quality_contract == EXACT_EMISSION_QUALITY and not actual and not shadow and event.limited: + if not admitted and self.quality_contract == ZERO_REFERENCE_QUALITY and not actual and not shadow and event.limited: + admitted = (witness.codec_id in EXACT_EMISSION_CODECS and witness.steer_requested is True and + type(raw) is int and type(reference_raw) is int and raw == reference_raw and + requested * witness.requested_torque > 0 and abs(requested) < abs(witness.requested_torque) and + zero_reference_relief_is_admitted(witness.codec_input_torque, reference_source, source, + recorded_output, reference_output, candidate_output, original_demand, reference_demand, candidate_demand)) + if not admitted and self.quality_contract in (EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY) and not actual and not shadow and event.limited: admitted = exact_local_emission(witness, reference_raw, raw, original_demand, reference_demand, candidate_demand) if not shadow and (not original_demand or not actual and not admitted): raise ValueError('candidate encoder clipping regression') @@ -213,10 +221,15 @@ class EncoderBranch: reference_demand = reference_request - reference_raw candidate_demand = requested_raw - raw admitted = debt_is_admitted(original_demand, reference_demand, candidate_demand, self.quality_contract) - if not admitted and self.quality_contract in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY) and not shadow and event.limited: + if not admitted and self.quality_contract in (STATE_ALIGNED_RELIEF, EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY) and not shadow and event.limited: admitted = native_relief_is_admitted(witness.unlimited_raw, reference_request, requested_raw, witness.final_raw, reference_raw, raw, original_demand, reference_demand, candidate_demand) - if not admitted and self.quality_contract == EXACT_EMISSION_QUALITY and not actual and not shadow and event.limited: + if not admitted and self.quality_contract == ZERO_REFERENCE_QUALITY and not actual and not shadow and event.limited: + admitted = (witness.codec_id in EXACT_EMISSION_CODECS and witness.steer_requested is True and + type(raw) is int and type(reference_raw) is int and raw == reference_raw and + zero_reference_relief_is_admitted(witness.unlimited_raw, reference_request, requested_raw, + witness.final_raw, reference_raw, raw, original_demand, reference_demand, candidate_demand)) + if not admitted and self.quality_contract in (EXACT_EMISSION_QUALITY, ZERO_REFERENCE_QUALITY) and not actual and not shadow and event.limited: admitted = exact_local_emission(witness, reference_raw, raw, original_demand, reference_demand, candidate_demand) if not actual and not shadow and not admitted: raise ValueError('candidate encoder clipping regression')