mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
80fbb4f6db
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior. Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com> Co-authored-by: Daniel Koepping <elkoled@gmail.com> Co-authored-by: Danny <dannyirwin31@yahoo.com> Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com> Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com> Co-authored-by: Kacper Rączy <gfw.kra@gmail.com> Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu> Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: stef <19478336+stefpi@users.noreply.github.com> Co-authored-by: whoisdomi <dcantu83@gmail.com> Co-authored-by: Intelli <contact@intelli.software> Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
167 lines
7.7 KiB
Python
167 lines
7.7 KiB
Python
"""Exact native planner behavior when unchanged MPC cost writes are skipped."""
|
|
|
|
from collections import Counter
|
|
from typing import Any, cast
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import numpy as np
|
|
|
|
from opendbc.car.honda.interface import CarInterface
|
|
from opendbc.car.honda.values import CAR
|
|
from openpilot.selfdrive.controls.lib import longitudinal_planner as planner_module
|
|
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib import long_mpc
|
|
from openpilot.starpilot.longitudinal.profile_runtime import ProfileTuning
|
|
from openpilot.starpilot.longitudinal.tests.test_cruise_ceiling import messages
|
|
|
|
|
|
WRITES_PER_UPDATE = 2 * (long_mpc.N + 1)
|
|
|
|
|
|
class SolverRecorder:
|
|
def __init__(self, native=None):
|
|
self.native = native
|
|
self.writes = Counter()
|
|
self.resets = 0
|
|
self.costs = {}
|
|
self.fail_at = None
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self.native, name)
|
|
|
|
def reset(self):
|
|
self.resets += 1
|
|
self.costs.clear()
|
|
return self.native.reset()
|
|
|
|
def cost_set(self, stage, field, data):
|
|
if field in ('W', 'Zl'):
|
|
self.writes[field] += 1
|
|
if sum(self.writes.values()) == self.fail_at:
|
|
raise RuntimeError('injected native cost write failure')
|
|
self.costs[stage, field] = np.array(data, copy=True)
|
|
if self.native is not None:
|
|
return self.native.cost_set(stage, field, data)
|
|
|
|
|
|
class AlwaysWriteMpc(long_mpc.LongitudinalMpc):
|
|
def set_cost_weights(self, cost_weights, constraint_cost_weights):
|
|
self._cached_cost_weights = None
|
|
super().set_cost_weights(cost_weights, constraint_cost_weights)
|
|
|
|
|
|
class TestMpcCostCache(unittest.TestCase):
|
|
def recording_mpc(self):
|
|
mpc = cast(Any, long_mpc.LongitudinalMpc.__new__(long_mpc.LongitudinalMpc))
|
|
mpc.solver = SolverRecorder()
|
|
mpc._cached_cost_weights = None
|
|
return mpc
|
|
|
|
def assert_array_bytes_equal(self, left, right):
|
|
self.assertEqual(left.shape, right.shape)
|
|
self.assertEqual(left.dtype, right.dtype)
|
|
self.assertTrue(np.isfinite(left).all())
|
|
self.assertTrue(np.isfinite(right).all())
|
|
self.assertEqual(left.tobytes(), right.tobytes())
|
|
|
|
def test_full_vectors_and_in_place_mutations_write_immediately(self):
|
|
mpc = self.recording_mpc()
|
|
cost, constraint = [3.0, 0.0, 0.0, 0.0, 200.0, 5.0], [1e6, 1e6, 1e6, 100.0]
|
|
mpc.set_cost_weights(cost, constraint)
|
|
count = WRITES_PER_UPDATE
|
|
self.assertEqual(sum(mpc.solver.writes.values()), count)
|
|
mpc.set_cost_weights(list(cost), list(constraint))
|
|
self.assertEqual(sum(mpc.solver.writes.values()), count)
|
|
for vector in (cost, constraint):
|
|
for index in range(len(vector)):
|
|
with self.subTest(vector='cost' if vector is cost else 'constraint', index=index):
|
|
vector[index] += 0.125
|
|
mpc.set_cost_weights(cost, constraint)
|
|
count += WRITES_PER_UPDATE
|
|
self.assertEqual(sum(mpc.solver.writes.values()), count)
|
|
mpc.set_cost_weights(cost, constraint)
|
|
self.assertEqual(sum(mpc.solver.writes.values()), count)
|
|
|
|
def test_partial_failed_write_invalidates_previous_key_before_restoring_it(self):
|
|
mpc = self.recording_mpc()
|
|
a, constraints = [3.0, 0.0, 0.0, 0.0, 200.0, 5.0], [1e6, 1e6, 1e6, 100.0]
|
|
b = [value + 1.0 for value in a]
|
|
mpc.set_cost_weights(a, constraints)
|
|
original = {key: value.copy() for key, value in mpc.solver.costs.items()}
|
|
mpc.solver.fail_at = WRITES_PER_UPDATE + long_mpc.N + 3
|
|
with self.assertRaisesRegex(RuntimeError, 'injected native'):
|
|
mpc.set_cost_weights(b, [value + 1.0 for value in constraints])
|
|
self.assertIsNone(mpc._cached_cost_weights)
|
|
self.assertNotEqual(mpc.solver.costs[0, 'W'].tobytes(), original[0, 'W'].tobytes())
|
|
count = sum(mpc.solver.writes.values())
|
|
mpc.set_cost_weights(a, constraints)
|
|
self.assertEqual(sum(mpc.solver.writes.values()), count + WRITES_PER_UPDATE)
|
|
for key, expected in original.items():
|
|
self.assert_array_bytes_equal(mpc.solver.costs[key], expected)
|
|
mpc.set_cost_weights(a, constraints)
|
|
self.assertEqual(sum(mpc.solver.writes.values()), count + WRITES_PER_UPDATE)
|
|
|
|
def test_native_reset_rewrites_unchanged_costs(self):
|
|
factory = long_mpc.AcadosOcpSolverCython
|
|
with patch.object(long_mpc, 'AcadosOcpSolverCython', side_effect=lambda *args: SolverRecorder(factory(*args))):
|
|
mpc = long_mpc.LongitudinalMpc()
|
|
recorder = cast(Any, mpc.solver)
|
|
self.assertEqual(sum(recorder.writes.values()), WRITES_PER_UPDATE)
|
|
mpc.set_weights()
|
|
self.assertEqual(sum(recorder.writes.values()), WRITES_PER_UPDATE)
|
|
mpc.reset()
|
|
self.assertEqual(recorder.resets, 2)
|
|
self.assertEqual(sum(recorder.writes.values()), 2 * WRITES_PER_UPDATE)
|
|
self.assertEqual(len(recorder.costs), WRITES_PER_UPDATE)
|
|
|
|
def test_actual_native_outputs_survive_settings_sources_and_resets_unchanged(self):
|
|
cp = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
|
factory = long_mpc.AcadosOcpSolverCython
|
|
with patch.object(long_mpc, 'AcadosOcpSolverCython', side_effect=lambda *args: SolverRecorder(factory(*args))):
|
|
actual = planner_module.LongitudinalPlanner(cp, init_v=20.0)
|
|
with patch.object(planner_module, 'LongitudinalMpc', AlwaysWriteMpc):
|
|
reference = planner_module.LongitudinalPlanner(cp, init_v=20.0)
|
|
sources = set()
|
|
for frame in range(320):
|
|
lead = 120 <= frame < 160 or 220 <= frame < 240
|
|
e2e, off, traffic = 160 <= frame < 200, 200 <= frame < 220, 240 <= frame < 280
|
|
personality = 'aggressive' if 220 <= frame < 240 else 'standard'
|
|
tuning = None
|
|
if 60 <= frame < 280:
|
|
tuning = ProfileTuning('traffic' if traffic else personality, 0.9 if traffic else 1.6,
|
|
1.3, 0.8, 1.2, 0.7, 1.1, 1.0 if traffic else 1.2, 0.42 if traffic else 1.1)
|
|
if frame in (180, 300):
|
|
reference.mpc.reset()
|
|
actual.mpc.reset()
|
|
for planner in (reference, actual):
|
|
sm, _ = messages(lead=lead, e2e=e2e, off=off)
|
|
sm['selfdriveState'].personality = personality
|
|
sm['carControl'].longActive = not off
|
|
sm['carState'].canValid = True
|
|
sm['carState'].aEgo = 0.01 if frame // 10 % 2 else -0.01
|
|
planner.update(sm, profile_tuning=tuning, traffic_mode=traffic, now_ns=100_000_000_000 + frame * 50_000_000)
|
|
self.assertEqual(planner.mpc.solution_status, 0)
|
|
with self.subTest(frame=frame):
|
|
for name in ('v_desired_trajectory', 'a_desired_trajectory', 'j_desired_trajectory'):
|
|
self.assert_array_bytes_equal(getattr(reference, name), getattr(actual, name))
|
|
for name in ('x_sol', 'u_sol', 'params', 'a_prev', 'x0'):
|
|
self.assert_array_bytes_equal(getattr(reference.mpc, name), getattr(actual.mpc, name))
|
|
for name in ('output_a_target', 'output_should_stop', 'a_cruise', 'allow_throttle', 'fcw', 'last_profile'):
|
|
self.assertEqual(getattr(reference, name), getattr(actual, name))
|
|
self.assertEqual(reference.mpc.source, actual.mpc.source)
|
|
expected_solver, actual_solver = cast(Any, reference.mpc.solver), cast(Any, actual.mpc.solver)
|
|
self.assertEqual(expected_solver.costs.keys(), actual_solver.costs.keys())
|
|
for key in expected_solver.costs:
|
|
self.assert_array_bytes_equal(expected_solver.costs[key], actual_solver.costs[key])
|
|
sources.add(actual.mpc.source)
|
|
self.assertEqual(sources, {long_mpc.LongitudinalPlanSource.cruise, long_mpc.LongitudinalPlanSource.lead0,
|
|
long_mpc.LongitudinalPlanSource.e2e})
|
|
self.assertEqual(expected_solver.resets, 3)
|
|
self.assertEqual(actual_solver.resets, 3)
|
|
self.assertEqual(sum(expected_solver.writes.values()), 323 * WRITES_PER_UPDATE)
|
|
self.assertEqual(sum(actual_solver.writes.values()), 33 * WRITES_PER_UPDATE)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|