sunnypilot v2026.002.000

version: sunnypilot v2026.002.000 (staging)
date: 2026-07-21T07:44:02
master commit: 2c334ede443d7391d27575af8c854a095ba702a8
This commit is contained in:
github-actions[bot] committed 2026-07-21 07:44:02 +00:00
commit de77c89cf6
3995 files changed
+1260688

No files matched your search

Whitespace-only changes.
@@ -0,0 +1,204 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pickle
import pytest
import openpilot.sunnypilot.models.helpers as helpers
import openpilot.sunnypilot.modeld_v2.modeld as modeld_module
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants
class DummyOverride:
def __init__(self, key, value):
self.key = key
self.value = value
class DummyArtifact:
def __init__(self, file_name):
self.fileName = file_name
class DummyModelType:
def __init__(self, raw):
self.raw = raw
class DummyModel:
def __init__(self, type_str, artifact_file):
self.type = DummyModelType(type_str)
self.artifact = DummyArtifact(artifact_file)
class DummyBundle:
def __init__(self, is_20hz=False, models=None, generation=10):
self.overrides = [DummyOverride('lat', '.1'), DummyOverride('long', '.3')]
self.generation = generation
self.is20hz = is_20hz
self.models = models or []
class Archetype:
def __init__(self, name, metadata_structure, model_stubs, is_20hz,
expected_model_type, expected_constants_class, expected_parser_module,
expected_desire_key):
self.name = name
self.metadata_structure = metadata_structure
self.model_stubs = model_stubs
self.is_20hz = is_20hz
self.expected_model_type = expected_model_type
self.expected_constants_class = expected_constants_class
self.expected_parser_module = expected_parser_module
self.expected_desire_key = expected_desire_key
def _noop_jit(**kwargs):
pass
def _make_vision_policy_metadata(vision_input_shapes, policy_input_shapes,
vision_output_slices, policy_output_slices):
return {
'vision': {'input_shapes': vision_input_shapes, 'output_slices': vision_output_slices},
'policy': {'input_shapes': policy_input_shapes, 'output_slices': policy_output_slices},
}
def _make_multi_policy_metadata(vision_input_shapes, policy_input_shapes,
vision_output_slices, policy_output_slices):
return {
'vision': {'input_shapes': vision_input_shapes, 'output_slices': vision_output_slices},
'offPolicy': {'input_shapes': policy_input_shapes, 'output_slices': policy_output_slices},
}
def _make_tri_policy_metadata(vision_input_shapes, policy_input_shapes,
vision_output_slices, policy_output_slices):
return {
'vision': {'input_shapes': vision_input_shapes, 'output_slices': vision_output_slices},
'onPolicy': {'input_shapes': policy_input_shapes, 'output_slices': policy_output_slices},
'offPolicy': {'input_shapes': policy_input_shapes, 'output_slices': policy_output_slices},
}
def _make_supercombo_metadata(input_shapes, output_slices):
return {'model': {'input_shapes': input_shapes, 'output_slices': output_slices}}
SPLIT_VISION_INPUT_SHAPES = {'img': (1, 12, 128, 256), 'big_img': (1, 12, 128, 256)}
SPLIT_POLICY_INPUT_SHAPES = {'features_buffer': (1, 25, 512), 'desire_pulse': (1, 25, 8), 'traffic_convention': (1, 2)}
SPLIT_VISION_SLICES = {'hidden_state': slice(0, 512), 'pose': slice(512, 524)}
SPLIT_POLICY_SLICES = {'plan': slice(0, 495), 'meta': slice(495, 550)}
SUPERCOMBO_INPUT_SHAPES = {
'img': (1, 12, 128, 256), 'big_img': (1, 12, 128, 256),
'desire': (1, 100, 8), 'features_buffer': (1, 99, 512),
'lateral_control_params': (1, 2), 'prev_desired_curv': (1, 100, 1),
'traffic_convention': (1, 2),
}
SUPERCOMBO_SLICES = {'plan': slice(0, 495), 'hidden_state': slice(495, 1007), 'meta': slice(1007, 1062)}
CAM_W, CAM_H = 1928, 1208
ARCHETYPES = {
'vision_policy_split': Archetype(
name='vision_policy_split',
metadata_structure=_make_vision_policy_metadata(
SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES,
SPLIT_VISION_SLICES, SPLIT_POLICY_SLICES),
model_stubs=[DummyModel('vision', 'driving_test_tinygrad.pkl'),
DummyModel('policy', 'driving_test_tinygrad.pkl')],
is_20hz=True,
expected_model_type='split',
expected_constants_class=SplitModelConstants,
expected_parser_module='parse_model_outputs_split',
expected_desire_key='desire',
),
'vision_multi_policy': Archetype(
name='vision_multi_policy',
metadata_structure=_make_multi_policy_metadata(
SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES,
SPLIT_VISION_SLICES, SPLIT_POLICY_SLICES),
model_stubs=[DummyModel('vision', 'driving_test_tinygrad.pkl'),
DummyModel('offPolicy', 'driving_test_tinygrad.pkl')],
is_20hz=True,
expected_model_type='multi_policy',
expected_constants_class=SplitModelConstants,
expected_parser_module='parse_model_outputs_split',
expected_desire_key='desire',
),
'tri_policy': Archetype(
name='tri_policy',
metadata_structure=_make_tri_policy_metadata(
SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES,
SPLIT_VISION_SLICES, SPLIT_POLICY_SLICES),
model_stubs=[DummyModel('vision', 'driving_test_tinygrad.pkl'),
DummyModel('onPolicy', 'driving_test_tinygrad.pkl'),
DummyModel('offPolicy', 'driving_test_tinygrad.pkl')],
is_20hz=True,
expected_model_type='multi_policy',
expected_constants_class=SplitModelConstants,
expected_parser_module='parse_model_outputs_split',
expected_desire_key='desire',
),
'supercombo_non20hz': Archetype(
name='supercombo_non20hz',
metadata_structure=_make_supercombo_metadata(SUPERCOMBO_INPUT_SHAPES, SUPERCOMBO_SLICES),
model_stubs=[DummyModel('supercombo', 'driving_test_tinygrad.pkl')],
is_20hz=False,
expected_model_type='supercombo',
expected_constants_class=ModelConstants,
expected_parser_module='parse_model_outputs',
expected_desire_key='desire',
),
}
def make_pkl_data(archetype):
return {
'metadata': archetype.metadata_structure,
(CAM_W, CAM_H): {'run_policy': _noop_jit, 'warp_enqueue': _noop_jit},
}
def write_pkl(tmp_path, archetype):
pkl_path = tmp_path / 'driving_test_tinygrad.pkl'
with open(pkl_path, 'wb') as f:
pickle.dump(make_pkl_data(archetype), f)
return pkl_path
def make_bundle(archetype):
return DummyBundle(
models=archetype.model_stubs,
is_20hz=archetype.is_20hz,
)
@pytest.fixture
def patch_modeld(monkeypatch):
def _patch(bundle):
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle, raising=False)
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle, raising=False)
return _patch
@pytest.fixture
def model_state_factory(tmp_path, monkeypatch, patch_modeld):
from openpilot.common.hardware import hw
def _create(archetype):
write_pkl(tmp_path, archetype)
bundle = make_bundle(archetype)
patch_modeld(bundle)
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
return modeld_module.ModelState(cam_w=CAM_W, cam_h=CAM_H)
return _create
@@ -0,0 +1,84 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.transformations.model import get_warp_matrix
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
class MockStruct:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def __getitem__(self, item):
return getattr(self, item)
class TestCameraOffset:
def setup_method(self):
self.camera_offset = CameraOffsetHelper()
self.dc = DEVICE_CAMERAS[('mici', 'os04c10')]
def test_smoothing(self):
self.camera_offset.set_offset(0.2)
sm = MockStruct(
deviceState=MockStruct(deviceType='mici'),
roadCameraState=MockStruct(sensor='os04c10'),
liveCalibration=MockStruct(rpyCalib=[0.0, 0.0, 0.0], height=[1.22])
)
intrinsics_main = self.dc.fcam.intrinsics
intrinsics_extra = self.dc.ecam.intrinsics
device_from_calib_euler = np.array([0.0, 0.0, 0.0], dtype=np.float32)
main_transform = get_warp_matrix(device_from_calib_euler, intrinsics_main, False).astype(np.float32)
extra_transform = get_warp_matrix(device_from_calib_euler, intrinsics_extra, True).astype(np.float32)
self.camera_offset.update(main_transform, extra_transform, sm, False)
np.testing.assert_almost_equal(self.camera_offset.actual_camera_offset, 0.02)
self.camera_offset.update(main_transform, extra_transform, sm, False)
np.testing.assert_almost_equal(self.camera_offset.actual_camera_offset, 0.038)
def test_camera_offset_(self):
intrinsics = self.dc.fcam.intrinsics
transform = np.eye(3, dtype=np.float32)
height = 1.22
offset = 0.1
cy = intrinsics[1, 2]
expected_shear = np.eye(3, dtype=np.float32)
expected_shear[0, 1] = offset / height
expected_shear[0, 2] = -offset / height * cy
result = CameraOffsetHelper.apply_camera_offset(transform, intrinsics, height, offset)
np.testing.assert_array_almost_equal(result, expected_shear)
def test_update(self):
sm = MockStruct(
deviceState=MockStruct(deviceType='mici'),
roadCameraState=MockStruct(sensor='os04c10'),
liveCalibration=MockStruct(rpyCalib=[0.0, 0.0, 0.0], height=[1.22])
)
intrinsics_main = self.dc.fcam.intrinsics
intrinsics_extra = self.dc.ecam.intrinsics
device_from_calib_euler = np.array([0.0, 0.0, 0.0], dtype=np.float32)
main_transform = get_warp_matrix(device_from_calib_euler, intrinsics_main, False).astype(np.float32)
extra_transform = get_warp_matrix(device_from_calib_euler, intrinsics_extra, True).astype(np.float32)
self.camera_offset.set_offset(0.0) # test default offset doesn't change transformation
main_out, extra_out = self.camera_offset.update(main_transform, extra_transform, sm, False)
np.testing.assert_array_equal(main_out, main_transform)
np.testing.assert_array_equal(extra_out, extra_transform)
self.camera_offset.set_offset(0.2) # test valid offset changes transformation
main_out, extra_out = self.camera_offset.update(main_transform, extra_transform, sm, False)
assert not np.array_equal(main_out, main_transform)
assert not np.array_equal(extra_out, extra_transform)
assert main_out[0, 1] != 0.0
assert main_out[0, 2] != 0.0
@@ -0,0 +1,260 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pytest
import openpilot.sunnypilot.models.helpers as helpers
import openpilot.sunnypilot.modeld_v2.modeld as modeld_module
from openpilot.sunnypilot.modeld_v2.modeld import _find_driving_pkl
from openpilot.sunnypilot.modeld_v2.tests.conftest import DummyModel, DummyBundle, ARCHETYPES, CAM_W, CAM_H, \
SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES
ModelState = modeld_module.ModelState
# Pkl discovery
class TestFindDrivingPkl:
def test_returns_none_when_no_bundle(self):
assert _find_driving_pkl(None) is None
def test_returns_none_when_no_models(self):
bundle = DummyBundle(models=[])
assert _find_driving_pkl(bundle) is None
def test_returns_none_when_pkl_not_on_disk(self):
bundle = DummyBundle(models=[
DummyModel('vision', 'driving_fof_tinygrad.pkl'),
DummyModel('policy', 'driving_fof_tinygrad.pkl'),
])
assert _find_driving_pkl(bundle) is None
def test_finds_pkl_by_artifact_name(self, tmp_path, monkeypatch):
(tmp_path / 'driving_fof_tinygrad.pkl').write_bytes(b'fake')
from openpilot.common.hardware import hw
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
bundle = DummyBundle(models=[
DummyModel('vision', 'driving_fof_tinygrad.pkl'),
DummyModel('policy', 'driving_fof_tinygrad.pkl'),
])
result = _find_driving_pkl(bundle)
assert result is not None
assert 'driving_fof_tinygrad.pkl' in result
# Init — assertion guard
class TestModelStateCombinedInit:
def test_asserts_when_no_pkl(self, monkeypatch):
bundle = DummyBundle(models=[], is_20hz=True)
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle, raising=False)
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle, raising=False)
with pytest.raises(AssertionError, match="No driving pkl found"):
ModelState(cam_w=CAM_W, cam_h=CAM_H)
class TestStockEquivalence:
def test_split_queue_keys_match_stock(self, model_state_factory):
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip
state = model_state_factory(ARCHETYPES['vision_policy_split'])
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
# action_t is a deep-model prerequisite the SP loader doesn't provide yet; see skip_keys below
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)}
stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY')
# TODO-SP: remove action_t skip once SP adds prerequisite for deep models (action_t input queue)
# prev_feat is a stock QCOM corruption workaround handled inside the SP loader's JIT path
skip_keys = {'action_t', 'prev_feat'}
# stock packs the per-key policy inputs into packed_npy_inputs; the npy views carry the individual keys
stock_queue_keys = set(stock_queues.keys())
if 'packed_npy_inputs' in stock_queue_keys:
stock_queue_keys.remove('packed_npy_inputs')
stock_queue_keys |= set(stock_npy.keys())
assert set(state.input_queues.keys()) == stock_queue_keys - skip_keys, \
f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={stock_queue_keys}"
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \
f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}"
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
policy_shapes_desire = {'features_buffer': (1, 25, 512), 'desire': (1, 25, 8), 'traffic_convention': (1, 2)}
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, policy_shapes_desire)
queues, npy = make_split_input_queues(SPLIT_VISION_INPUT_SHAPES, policy_shapes_desire, frame_skip, device='NPY')
assert 'desire_q' in queues
assert 'desire' in npy
assert 'img_q' in queues
assert 'feat_q' in queues
def test_split_vision_input_names_match_stock(self, model_state_factory):
state = model_state_factory(ARCHETYPES['vision_policy_split'])
assert state.vision_input_names == ['img', 'big_img']
def test_split_output_slices_preserved(self, model_state_factory):
arch = ARCHETYPES['vision_policy_split']
state = model_state_factory(arch)
assert state.vision_output_slices == arch.metadata_structure['vision']['output_slices']
assert state.policy_output_slices == arch.metadata_structure['policy']['output_slices']
ARCHETYPE_NAMES = list(ARCHETYPES.keys())
class TestModelTypeDetection:
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_combined_model_type(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert state._combined_model_type == arch.expected_model_type, \
f"{arch.name}: got {state._combined_model_type}, expected {arch.expected_model_type}"
class TestConstantsSelection:
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_constants_class(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert type(state.constants) is arch.expected_constants_class, \
f"{arch.name}: got {type(state.constants).__name__}, expected {arch.expected_constants_class.__name__}"
class TestParserSelection:
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_parser_module(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
parser_module = type(state.parser).__module__
assert parser_module.endswith(arch.expected_parser_module), \
f"{arch.name}: parser from {parser_module}, expected module ending with {arch.expected_parser_module}"
class TestDesireKeyDetection:
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_desire_key(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert state.desire_key == arch.expected_desire_key, \
f"{arch.name}: got {state.desire_key}, expected {arch.expected_desire_key}"
class TestVisionInputNames:
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_vision_names_contain_img(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert len(state.vision_input_names) >= 1
for name in state.vision_input_names:
assert 'img' in name, f"{arch.name}: vision input name '{name}' missing 'img'"
class TestOutputSlices:
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_vision_slices_populated(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert len(state.vision_output_slices) > 0, f"{arch.name}: vision_output_slices empty"
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_policy_slices_match_type(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
if arch.expected_model_type == 'supercombo':
assert state.policy_output_slices == {}, f"{arch.name}: supercombo should have empty policy slices"
else:
assert len(state.policy_output_slices) > 0, f"{arch.name}: split/multi should have policy slices"
class TestInputQueueCreation:
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_queues_not_empty(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert len(state.input_queues) > 0, f"{arch.name}: input_queues empty"
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_npy_contains_transforms(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert 'tfm' in state.numpy_inputs, f"{arch.name}: 'tfm' missing from npy"
assert 'big_tfm' in state.numpy_inputs, f"{arch.name}: 'big_tfm' missing from npy"
assert state.numpy_inputs['tfm'].shape == (3, 3)
assert state.numpy_inputs['big_tfm'].shape == (3, 3)
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_npy_contains_desire(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert arch.expected_desire_key in state.numpy_inputs, \
f"{arch.name}: '{arch.expected_desire_key}' missing from npy"
class TestFrameBufferParams:
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_frame_buf_params_per_vision_input(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
for name in state.vision_input_names:
assert name in state.frame_buf_params, f"{arch.name}: frame_buf_params missing '{name}'"
nv12_info = state.frame_buf_params[name]
assert len(nv12_info) >= 4, f"{arch.name}: nv12_info for '{name}' too short"
class TestBundleOverrides:
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_smoothing_params_from_overrides(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert state.LAT_SMOOTH_SECONDS == 0.1
assert state.LONG_SMOOTH_SECONDS == 0.3
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_generation_from_bundle(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert state.generation == 10
class TestMlsimProperty:
def test_mlsim_false_for_gen10(self, model_state_factory):
state = model_state_factory(ARCHETYPES['supercombo_non20hz'])
assert state.mlsim is False
def test_mlsim_true_for_gen11(self, tmp_path, monkeypatch, patch_modeld):
from openpilot.common.hardware import hw
from openpilot.sunnypilot.modeld_v2.tests.conftest import write_pkl, ARCHETYPES as A
arch = A['supercombo_non20hz']
write_pkl(tmp_path, arch)
bundle = DummyBundle(models=arch.model_stubs, is_20hz=arch.is_20hz, generation=11)
patch_modeld(bundle)
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
state = ModelState(cam_w=CAM_W, cam_h=CAM_H)
assert state.mlsim is True
class TestCrossArchetypeMismatch:
def test_wrong_is_20hz_changes_constants(self, tmp_path, monkeypatch, patch_modeld):
from openpilot.common.hardware import hw
from openpilot.sunnypilot.modeld_v2.tests.conftest import write_pkl
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
arch = ARCHETYPES['vision_policy_split']
write_pkl(tmp_path, arch)
bundle = DummyBundle(models=arch.model_stubs, is_20hz=False)
patch_modeld(bundle)
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
state = ModelState(cam_w=CAM_W, cam_h=CAM_H)
assert type(state.constants) is ModelConstants, \
"Wrong is_20hz should produce wrong constants class"
@@ -0,0 +1,161 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
import pytest
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key
class TestDeriveFrameSkip:
def test_non20hz_supercombo(self):
vision = {}
policy = {'features_buffer': (1, 99, 512), 'desire': (1, 100, 8)}
assert derive_frame_skip(vision, policy) == 1
def test_20hz_supercombo(self):
vision = {}
policy = {'features_buffer': (1, 24, 512), 'desire': (1, 25, 8)}
assert derive_frame_skip(vision, policy) == 4
def test_split_vision_policy(self):
vision = {'img': (1, 12, 128, 256)}
policy = {'features_buffer': (1, 25, 512), 'desire_pulse': (1, 25, 8)}
assert derive_frame_skip(vision, policy) == 4
def test_no_features_buffer(self):
assert derive_frame_skip({}, {}) == 1
class TestFrameSkipBufferLengthEquivalence:
@pytest.mark.parametrize("frame_skip,expected_buffer_length", [
(1, 2),
(4, 5),
])
def test_img_buffer_size_matches_warp_buffer_length(self, frame_skip, expected_buffer_length):
n_frames = 2
img_buf_dim0 = frame_skip * (n_frames - 1) + 1
assert img_buf_dim0 == expected_buffer_length, \
f"frame_skip={frame_skip}: img_buf[0]={img_buf_dim0}, expected {expected_buffer_length}"
@pytest.mark.parametrize("is_20hz,expected_frame_skip,expected_buffer_length", [
(False, 1, 2),
(True, 4, 5),
])
def test_is_20hz_to_frame_skip_to_buffer_length(self, is_20hz, expected_frame_skip, expected_buffer_length):
if is_20hz:
policy_shapes = {'features_buffer': (1, 24, 512)}
else:
policy_shapes = {'features_buffer': (1, 99, 512)}
frame_skip = derive_frame_skip({}, policy_shapes)
assert frame_skip == expected_frame_skip
n_frames = 2
img_buf_dim0 = frame_skip * (n_frames - 1) + 1
assert img_buf_dim0 == expected_buffer_length
class TestTemporalSamplingEquivalence:
def test_non20hz_desire_sampling_identity(self):
buf = np.random.randn(100, 1, 8).astype(np.float32)
frame_skip = 1
sampled = buf[::frame_skip].reshape(-1, 8)
assert sampled.shape == (100, 8)
np.testing.assert_array_equal(sampled, buf[:, 0, :])
def test_20hz_desire_sampling_max(self):
buf = np.zeros((100, 1, 8), dtype=np.float32)
buf[99, 0, 3] = 1.0
frame_skip = 4
reshaped = buf.reshape(-1, frame_skip, 1, 8).max(axis=1)
sampled = reshaped.reshape(-1, 8)
assert sampled.shape == (25, 8)
assert sampled[24, 3] == 1.0
assert sampled[23, 3] == 0.0
def test_split_features_buffer_sampling_skip(self):
buf = np.arange(100 * 512, dtype=np.float32).reshape(100, 1, 512)
frame_skip = 4
sampled = buf[::frame_skip].reshape(-1, 512)
assert sampled.shape == (25, 512)
np.testing.assert_array_equal(sampled[0], buf[0, 0])
np.testing.assert_array_equal(sampled[1], buf[4, 0])
np.testing.assert_array_equal(sampled[24], buf[96, 0])
def test_non20hz_features_buffer_sampling_identity(self):
buf = np.arange(99 * 512, dtype=np.float32).reshape(99, 1, 512)
frame_skip = 1
sampled = buf[::frame_skip].reshape(-1, 512)
assert sampled.shape == (99, 512)
np.testing.assert_array_equal(sampled, buf[:, 0, :])
class TestTemporalIdxEquivalence:
@pytest.mark.parametrize("mode,desire_shape,fb_shape,frame_skip", [
('non20hz', (1, 100, 8), (1, 99, 512), 1),
('20hz', (1, 25, 8), (1, 24, 512), 4),
('split', (1, 25, 8), (1, 25, 512), 4),
])
def test_features_buffer_idx_equivalence(self, mode, desire_shape, fb_shape, frame_skip):
history = fb_shape[1]
if mode == 'non20hz':
modelstate_idxs = np.arange(history)
buf_len = history
elif mode == '20hz':
buf_len = (history + 1) * 4
step = int(-buf_len / history)
modelstate_idxs = np.arange(step, step * (history + 1), step)[::-1]
elif mode == 'split':
buf_len = history * 4
skip = buf_len // history
modelstate_idxs = np.arange(buf_len)[-1 - (skip * (history - 1))::skip]
assert len(modelstate_idxs) == fb_shape[1], \
f"{mode}: ModelState idx count {len(modelstate_idxs)} != input shape {fb_shape[1]}"
@pytest.mark.parametrize("mode,desire_shape,fb_shape,frame_skip", [
('non20hz', (1, 100, 8), (1, 99, 512), 1),
('20hz', (1, 25, 8), (1, 24, 512), 4),
('split', (1, 25, 8), (1, 25, 512), 4),
])
def test_desire_idx_equivalence(self, mode, desire_shape, fb_shape, frame_skip):
history = desire_shape[1]
compile_desire_buf_len = frame_skip * history if mode != 'non20hz' else history
compile_sampled_count = compile_desire_buf_len // frame_skip if frame_skip > 1 else compile_desire_buf_len
assert compile_sampled_count == history, \
f"{mode}: compile desire samples {compile_sampled_count} != model input {history}"
class TestDetectDesireKey:
def test_finds_desire(self):
shapes = {'features_buffer': (1, 99, 512), 'desire': (1, 100, 8), 'traffic_convention': (1, 2)}
assert _detect_desire_key(shapes) == 'desire'
def test_finds_desire_pulse(self):
shapes = {'features_buffer': (1, 25, 512), 'desire_pulse': (1, 25, 8), 'traffic_convention': (1, 2)}
assert _detect_desire_key(shapes) == 'desire_pulse'
def test_returns_none_when_no_desire(self):
shapes = {'features_buffer': (1, 99, 512), 'traffic_convention': (1, 2)}
assert _detect_desire_key(shapes) is None
class TestOutputSlicePreservation:
def test_vision_hidden_state_slice_used_for_features(self):
mock_slices = {'hidden_state': slice(0, 512), 'plan': slice(512, 1024)}
features_slice = mock_slices['hidden_state']
fake_output = np.random.randn(1, 1024).astype(np.float32)
features = fake_output[:, features_slice]
assert features.shape == (1, 512)
def test_policy_output_slices_independent(self):
vision_slices = {'hidden_state': slice(0, 512)}
policy_slices = {'plan': slice(0, 495), 'meta': slice(495, 550)}
assert set(vision_slices.keys()) & set(policy_slices.keys()) == set(), \
"vision and policy slices should not overlap in keys"
@@ -0,0 +1,70 @@
import numpy as np
from openpilot.cereal import log
from openpilot.sunnypilot.modeld_v2.constants import Plan
from openpilot.sunnypilot.modeld_v2.modeld import ModelState
import openpilot.sunnypilot.modeld_v2.modeld as modeld
class MockStruct:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def test_recovery_power_scaling():
state = MockStruct(
PLANPLUS_CONTROL=0.75,
LONG_SMOOTH_SECONDS=0.3,
LAT_SMOOTH_SECONDS=0.1,
MIN_LAT_CONTROL_SPEED=0.3,
mlsim=True,
generation=12,
constants=MockStruct(T_IDXS=np.arange(100), DESIRE_LEN=8)
)
prev_action = log.ModelDataV2.Action()
recorded_vel: list = []
recorded_curv_plans: list = []
def mock_accel(plan_vel, plan_accel, t_idxs, action_t=0.0):
recorded_vel.append(plan_vel.copy())
return 0.0, False
def mock_curvature(output, plan, vego, lat_action_t, mlsim):
recorded_curv_plans.append(plan.copy())
return 0.0
modeld.get_accel_from_plan = mock_accel
modeld.get_curvature_from_output = mock_curvature
plan = np.random.rand(1, 100, 15).astype(np.float32)
planplus = np.random.rand(1, 100, 15).astype(np.float32)
merged_plan = plan + planplus
model_output: dict = {
'plan': merged_plan.copy(),
'planplus': planplus.copy()
}
test_cases: list = [
# (control, v_ego)
(0.55, 20.0),
(1.0, 25.0),
(1.5, 25.1),
(2.0, 20.0),
(0.75, 19.0),
(0.8, 25.1),
]
for control, v_ego in test_cases:
state.PLANPLUS_CONTROL = control
recorded_vel.clear()
recorded_curv_plans.clear()
ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego)
expected_accel_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + planplus[0, :, Plan.VELOCITY][:, 0]
np.testing.assert_allclose(recorded_vel[0], expected_accel_plan_vel, rtol=1e-5, atol=1e-6)
# For the below, yes, I know this isn't the same slicing as fillmodlmsg. This is to show that the values are only scaled on curv
expected_curv_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + control * planplus[0, :, Plan.VELOCITY][:, 0]
np.testing.assert_allclose(recorded_curv_plans[0][:, Plan.VELOCITY][:, 0], expected_curv_plan_vel, rtol=1e-5, atol=1e-6)
@@ -0,0 +1,103 @@
import os
os.environ['DEV'] = 'CPU'
import pytest
import numpy as np
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.sunnypilot.modeld_v2.warp import CAMERA_CONFIGS
from openpilot.sunnypilot.modeld_v2.warp import Warp, MODEL_W, MODEL_H
VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs
('img', 'big_img'),
('input_imgs', 'big_input_imgs'),
]
class MockVisionBuf:
def __init__(self, w, h):
self.width = w
self.height = h
_, _, _, yuv_size = get_nv12_info(w, h)
self.data = np.zeros(yuv_size, dtype=np.uint8)
@pytest.mark.parametrize("buffer_length", [2, 5])
def test_warp_initialization(buffer_length):
warp = Warp(buffer_length)
assert warp.buffer_length == buffer_length
assert warp.img_buffer_shape == (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
@pytest.mark.parametrize("buffer_length", [2, 5])
@pytest.mark.parametrize("cam_w, cam_h", CAMERA_CONFIGS)
@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS)
def test_warp_process(buffer_length, cam_w, cam_h, road, wide):
warp = Warp(buffer_length)
mock_buf = MockVisionBuf(cam_w, cam_h)
transform = np.eye(3, dtype=np.float32).flatten()
bufs = {road: mock_buf, wide: mock_buf}
transforms = {road: transform, wide: transform}
out = warp.process(bufs, transforms)
assert isinstance(out, dict)
assert road in out and wide in out
assert out[road].shape == (1, 12, MODEL_H // 2, MODEL_W // 2)
assert out[wide].shape == (1, 12, MODEL_H // 2, MODEL_W // 2)
key = (cam_w, cam_h)
assert key in warp.jit_cache
out2 = warp.process(bufs, transforms)
assert out2[road].shape == out[road].shape
@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS)
def test_warp_buffer_shift(road, wide):
warp = Warp(2)
cam_w, cam_h = CAMERA_CONFIGS[1]
transform = np.eye(3, dtype=np.float32).flatten()
buf1 = MockVisionBuf(cam_w, cam_h)
buf1.data[0] = 255
bufs1 = {road: buf1, wide: buf1}
transforms = {road: transform, wide: transform}
out1 = warp.process(bufs1, transforms)
road1 = out1[road].numpy().copy()
buf2 = MockVisionBuf(cam_w, cam_h)
buf2.data[0] = 128
bufs2 = {road: buf2, wide: buf2}
out2 = warp.process(bufs2, transforms)
assert not np.array_equal(road1, out2[road].numpy())
@pytest.mark.parametrize("buffer_length", [2, 5])
@pytest.mark.parametrize("road, wide", VISION_NAME_PAIRS)
def test_warp_buffer_accumulation(buffer_length, road, wide):
warp = Warp(buffer_length)
cam_w, cam_h = CAMERA_CONFIGS[0]
transform = np.eye(3, dtype=np.float32).flatten()
transforms = {road: transform, wide: transform}
outputs = []
for i in range(buffer_length + 1):
buf = MockVisionBuf(cam_w, cam_h)
buf.data[:] = i * 10
out = warp.process({road: buf, wide: buf}, transforms)
outputs.append(out[road].numpy().copy())
assert warp.full_buffers['img'].shape == (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
for i in range(1, len(outputs)):
assert not np.array_equal(outputs[i - 1], outputs[i])
def test_warp_different_cameras_same_instance():
warp = Warp(2)
transform = np.eye(3, dtype=np.float32).flatten()
buf1 = MockVisionBuf(*CAMERA_CONFIGS[0])
warp.process({'img': buf1, 'big_img': buf1}, {'img': transform, 'big_img': transform})
assert len(warp.jit_cache) == 1
buf2 = MockVisionBuf(*CAMERA_CONFIGS[1])
warp.process({'img': buf2, 'big_img': buf2}, {'img': transform, 'big_img': transform})
assert len(warp.jit_cache) == 2