feat(perception): expire costmap permissions per cell
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""Bounded synthetic per-cell freshness tests, with no model/source dependency."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.perception.costmap_freshness import CostmapCellEvidence
|
||||
from k1link.perception.realtime_contract import RealtimeContractError
|
||||
|
||||
NOW = 9_007_199_254_740_993
|
||||
|
||||
|
||||
def check(stamps, states, actions, *, elapsed_ms=0, uncertainty_ms=0):
|
||||
return CostmapCellEvidence(tuple(stamps)).assess(
|
||||
states=tuple(states),
|
||||
actions=tuple(actions),
|
||||
source_time_ns=NOW,
|
||||
observed_source_time_ns=NOW + elapsed_ms * 1_000_000,
|
||||
maximum_age_ms=250,
|
||||
clock_uncertainty_ms=uncertainty_ms,
|
||||
)
|
||||
|
||||
|
||||
def test_expiry_is_addressed_and_cannot_clear_occupied_or_unknown_cells():
|
||||
stamps = (NOW - 200_000_000, NOW, NOW - 999_000_000, None, None)
|
||||
result = check(stamps, [1, 1, 2, 0, 1], [0, 1, 2, 2, 0], elapsed_ms=51)
|
||||
assert result.states == (3, 1, 2, 0, 3)
|
||||
assert result.actions == (2, 1, 2, 2, 2)
|
||||
assert result.expired_cells == 1 and result.missing_support_cells == 1
|
||||
assert result.suppressed_permissions == 2
|
||||
assert result.oldest_permissive_source_time_ns == NOW
|
||||
# Even an earlier assessment cannot restore a suppressed permission.
|
||||
repeated = check(stamps, result.states, result.actions)
|
||||
assert repeated.states == result.states and repeated.actions == result.actions
|
||||
|
||||
|
||||
def test_ttl_boundary_includes_mapping_uncertainty_without_refreshing_support():
|
||||
stamp = NOW - 200_000_000
|
||||
assert check([stamp], [1], [0], elapsed_ms=45, uncertainty_ms=5).actions == (0,)
|
||||
assert check([stamp], [1], [0], elapsed_ms=46, uncertainty_ms=5).actions == (2,)
|
||||
assert check([stamp], [1], [0]).oldest_permissive_source_time_ns == stamp
|
||||
|
||||
|
||||
def test_cell_wire_is_exact_int64_and_has_a_bounded_shape():
|
||||
evidence = CostmapCellEvidence((NOW, None))
|
||||
assert evidence.to_dict()["observed_source_time_ns"] == [str(NOW), None]
|
||||
assert CostmapCellEvidence.from_dict(json.loads(json.dumps(evidence.to_dict()))) == evidence
|
||||
with pytest.raises(RealtimeContractError):
|
||||
CostmapCellEvidence((NOW,) * 8193)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stamp", [NOW, "01", "-1", str(2**63), True, {}, "NaN"])
|
||||
def test_invalid_wire_timestamp_is_rejected(stamp):
|
||||
value = CostmapCellEvidence((NOW,)).to_dict()
|
||||
value["observed_source_time_ns"] = [stamp]
|
||||
with pytest.raises(RealtimeContractError):
|
||||
CostmapCellEvidence.from_dict(value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stamps,states,actions,elapsed,uncertainty",
|
||||
[
|
||||
([NOW + 1], [1], [0], 0, 0),
|
||||
([NOW], [1], [0], -1, 0),
|
||||
([NOW], [1], [0], 0, float("nan")),
|
||||
([NOW], [1], [0], 0, -1),
|
||||
([NOW], [True], [0], 0, 0),
|
||||
([NOW], [1], [True], 0, 0),
|
||||
([NOW], [2], [0], 0, 0),
|
||||
([None], [0], [1], 0, 0),
|
||||
([NOW], [], [], 0, 0),
|
||||
],
|
||||
)
|
||||
def test_invalid_evidence_never_grants_permission(stamps, states, actions, elapsed, uncertainty):
|
||||
with pytest.raises(RealtimeContractError):
|
||||
check(stamps, states, actions, elapsed_ms=elapsed, uncertainty_ms=uncertainty)
|
||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.perception.costmap_freshness import CostmapCellEvidence
|
||||
from k1link.perception.realtime_contract import (
|
||||
REQUIRED_LAYERS,
|
||||
LayerEvidence,
|
||||
@@ -296,3 +297,112 @@ def test_pilot_rejects_wrong_run_sequence_and_control_authority(pilot):
|
||||
changed["commands_enabled"] = True
|
||||
with pytest.raises(ValueError, match="authority"):
|
||||
pilot.validate_receipt(changed, bundle, epoch_id="pilot")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("own_time", [NOW + 1, True])
|
||||
def test_derivation_cannot_hide_invalid_own_time_behind_an_older_parent(own_time):
|
||||
value = scene(held_ms=100)
|
||||
with pytest.raises(RealtimeContractError):
|
||||
derive_layer(
|
||||
"costmap",
|
||||
epoch_id="epoch",
|
||||
source_sequence=2,
|
||||
source_time_ns=NOW,
|
||||
payload_sha256="a" * 64,
|
||||
inputs=tuple(value.layers[i] for i in (0, 2, 3)),
|
||||
oldest_required_input_time_ns=own_time,
|
||||
)
|
||||
|
||||
|
||||
def cell_input():
|
||||
payload, bundle, ddr = pilot_input()
|
||||
payload["costmap_states"] = [1, 1]
|
||||
payload["costmap_material"] = [1, 1]
|
||||
payload["policy_actions"] = [0, 0]
|
||||
payload["policy_counts"] = {"ALLOW_candidate": 2, "HIGH_COST": 0, "NO_GO": 0}
|
||||
payload["costmap_cell_evidence"] = CostmapCellEvidence((NOW - 200_000_000, NOW)).to_dict()
|
||||
payload["tgs_counts"]["oldest_permissive_cell_source_ns"] = NOW - 200_000_000
|
||||
return payload, bundle, ddr
|
||||
|
||||
|
||||
def test_consumer_expires_only_old_cell_and_rehashes_view_without_mutating_wire(pilot):
|
||||
payload, bundle, ddr = cell_input()
|
||||
pilot.prepare_publication(
|
||||
payload,
|
||||
bundle,
|
||||
ddr,
|
||||
epoch_id="pilot",
|
||||
now_ns=bundle["due_ns"] + 10_000_000,
|
||||
mode="per-cell",
|
||||
)
|
||||
original = copy.deepcopy(payload)
|
||||
freshness = pilot.validate_receipt(payload, bundle, epoch_id="pilot")
|
||||
view, checked = pilot.assess_receipt(
|
||||
payload,
|
||||
freshness,
|
||||
bundle=bundle,
|
||||
now_ns=bundle["due_ns"] + 60_000_000,
|
||||
)
|
||||
assert checked.fresh_complete
|
||||
assert view["costmap_states"] == [3, 1] and view["policy_actions"] == [2, 0]
|
||||
assert view["cell_assessment"]["expired_ground_cells"] == 1
|
||||
assert checked.layers[4].oldest_required_input_time_ns == NOW - 20_000_000
|
||||
assert payload == original
|
||||
derived = pilot.validate_receipt(view, bundle, epoch_id="pilot")
|
||||
assert derived.source_time_ns == freshness.source_time_ns
|
||||
aged, expired = pilot.assess_receipt(
|
||||
view,
|
||||
derived,
|
||||
bundle=bundle,
|
||||
now_ns=bundle["due_ns"] + 260_000_000,
|
||||
)
|
||||
assert not expired.fresh_complete and aged["policy_actions"] == [2, 2]
|
||||
pilot.validate_receipt(aged, bundle, epoch_id="pilot")
|
||||
with pytest.raises(ValueError, match="backwards"):
|
||||
pilot.assess_receipt(view, derived, bundle=bundle, now_ns=bundle["due_ns"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("missing,held_ms", [(True, 0), (False, 220)])
|
||||
def test_cell_freshness_cannot_override_missing_lidar_or_stale_segmentation(
|
||||
pilot, missing, held_ms
|
||||
):
|
||||
payload, bundle, ddr = cell_input()
|
||||
bundle["available"] = not missing
|
||||
if held_ms:
|
||||
ddr.update(state="held", source_sequence=1, source_host_monotonic_ns=NOW - 220_000_000)
|
||||
pilot.prepare_publication(
|
||||
payload,
|
||||
bundle,
|
||||
ddr,
|
||||
epoch_id="pilot",
|
||||
now_ns=bundle["due_ns"],
|
||||
mode="per-cell",
|
||||
)
|
||||
fresh = pilot.validate_receipt(payload, bundle, epoch_id="pilot")
|
||||
view, checked = pilot.assess_receipt(
|
||||
payload,
|
||||
fresh,
|
||||
bundle=bundle,
|
||||
now_ns=bundle["due_ns"] + 60_000_000,
|
||||
)
|
||||
assert not checked.fresh_complete and view["policy_actions"] == [2, 2]
|
||||
pilot.validate_receipt(view, bundle, epoch_id="pilot")
|
||||
|
||||
|
||||
def test_cell_timestamp_and_mode_are_bound_to_costmap_hash(pilot):
|
||||
payload, bundle, ddr = cell_input()
|
||||
pilot.prepare_publication(
|
||||
payload,
|
||||
bundle,
|
||||
ddr,
|
||||
epoch_id="pilot",
|
||||
now_ns=bundle["due_ns"],
|
||||
mode="per-cell",
|
||||
)
|
||||
changed = copy.deepcopy(payload)
|
||||
changed["costmap_cell_evidence"]["observed_source_time_ns"][0] = str(NOW)
|
||||
with pytest.raises(ValueError, match="digest"):
|
||||
pilot.validate_receipt(changed, bundle, epoch_id="pilot")
|
||||
payload["costmap_freshness_mode"] = "whole-scene"
|
||||
with pytest.raises(ValueError, match="digest"):
|
||||
pilot.validate_receipt(payload, bundle, epoch_id="pilot")
|
||||
|
||||
Reference in New Issue
Block a user