feat(perception): add occupied-only low-step shadow
This commit is contained in:
@@ -0,0 +1,643 @@
|
||||
"""Occupied-only low-step geometry shadow for M4.8R3.
|
||||
|
||||
The provider composes the accepted geometry association provider and may only
|
||||
add class-free current LiDAR observations. It never removes baseline evidence,
|
||||
publishes free space, changes camera semantics, or performs inference.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Final
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from .contracts import (
|
||||
EvidenceBasis,
|
||||
EvidenceCurrentness,
|
||||
MetricGeometry,
|
||||
ObjectProposal2D,
|
||||
ObstacleObservation,
|
||||
validate_exclusive_point_ownership,
|
||||
)
|
||||
from .geometry import (
|
||||
GeometryProviderSnapshot,
|
||||
Ravnoves00GeometryAssociationProvider,
|
||||
RecordedGeometryStore,
|
||||
)
|
||||
from .geometry_math import POINT_OCCUPIED
|
||||
from .providers import SourcePacket
|
||||
|
||||
M48_LOW_STEP_PROFILE_SCHEMA: Final = (
|
||||
"missioncore.m48-additive-low-step-occupancy-profile/v1"
|
||||
)
|
||||
M48_LOW_STEP_PROVIDER_ID: Final = "ravnoves00-additive-low-step-geometry/v1"
|
||||
|
||||
IntArray = npt.NDArray[np.int64]
|
||||
|
||||
|
||||
class M48LowStepOccupancyError(RuntimeError):
|
||||
"""The additive profile, source evidence, or bounded component set is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LowStepComponentProfile:
|
||||
voxel_size_m: float
|
||||
neighbor_radius_cells: int
|
||||
minimum_points: int
|
||||
minimum_voxels: int
|
||||
local_radius_m: float
|
||||
maximum_candidate_points_per_frame: int
|
||||
maximum_cells_per_component: int
|
||||
maximum_components_per_frame: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if (
|
||||
not math.isfinite(self.voxel_size_m)
|
||||
or not 0.05 <= self.voxel_size_m <= 2.0
|
||||
or self.neighbor_radius_cells != 1
|
||||
or not 1 <= self.minimum_points <= 256
|
||||
or not 1 <= self.minimum_voxels <= 128
|
||||
or not math.isfinite(self.local_radius_m)
|
||||
or not 1.0 <= self.local_radius_m <= 100.0
|
||||
or not 1 <= self.maximum_candidate_points_per_frame <= 4096
|
||||
or not 1 <= self.maximum_cells_per_component <= 2048
|
||||
or not 1 <= self.maximum_components_per_frame <= 512
|
||||
):
|
||||
raise M48LowStepOccupancyError("low-step component bounds are invalid")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LowStepSeparationExpectation:
|
||||
anchor_id: str
|
||||
sequence: int
|
||||
expected_minimum_components: int
|
||||
interpretation: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M48LowStepOccupancyProfile:
|
||||
profile_id: str
|
||||
provider_id: str
|
||||
source_id: str
|
||||
session_id: str
|
||||
frame_count: int
|
||||
point_count: int
|
||||
local_surface_model_id: str
|
||||
local_surface_sha256: str
|
||||
base_geometry_profile_id: str
|
||||
base_geometry_profile_sha256: str
|
||||
component: LowStepComponentProfile
|
||||
separation_expectations: tuple[LowStepSeparationExpectation, ...]
|
||||
profile_sha256: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M48LowStepOccupancySnapshot:
|
||||
base: GeometryProviderSnapshot
|
||||
input_frames: int
|
||||
completed_frames: int
|
||||
failed_frames: int
|
||||
frames_with_additions: int
|
||||
candidate_point_count: int
|
||||
additive_observation_count: int
|
||||
additive_voxel_count: int
|
||||
peak_candidate_points_per_frame: int
|
||||
peak_additive_observations_per_frame: int
|
||||
peak_voxels_per_component: int
|
||||
additive_core_duration_ns: int
|
||||
|
||||
|
||||
class M48AdditiveLowStepGeometryProvider:
|
||||
"""Compose baseline geometry with bounded, spatially separate step components."""
|
||||
|
||||
provider_id: str = M48_LOW_STEP_PROVIDER_ID
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
store: RecordedGeometryStore,
|
||||
profile: M48LowStepOccupancyProfile,
|
||||
) -> None:
|
||||
if profile.provider_id != self.provider_id:
|
||||
raise M48LowStepOccupancyError("low-step provider identity changed")
|
||||
geometry = store.profile
|
||||
if (
|
||||
geometry.source_id != profile.source_id
|
||||
or geometry.session_id != profile.session_id
|
||||
or geometry.frame_count != profile.frame_count
|
||||
or geometry.point_count != profile.point_count
|
||||
or geometry.local_surface_model_id != profile.local_surface_model_id
|
||||
or geometry.local_surface_sha256 != profile.local_surface_sha256
|
||||
or geometry.profile_id != profile.base_geometry_profile_id
|
||||
or geometry.profile_sha256 != profile.base_geometry_profile_sha256
|
||||
):
|
||||
raise M48LowStepOccupancyError("low-step source binding changed")
|
||||
self.store = store
|
||||
self.profile = profile
|
||||
self.base = Ravnoves00GeometryAssociationProvider(store=store)
|
||||
self._lock = Lock()
|
||||
self._input_frames = 0
|
||||
self._completed_frames = 0
|
||||
self._failed_frames = 0
|
||||
self._frames_with_additions = 0
|
||||
self._candidate_points = 0
|
||||
self._additive_observation_count = 0
|
||||
self._additive_voxels = 0
|
||||
self._peak_candidate_points = 0
|
||||
self._peak_additive_observations = 0
|
||||
self._peak_component_voxels = 0
|
||||
self._additive_core_duration_ns = 0
|
||||
|
||||
def associate(
|
||||
self,
|
||||
packet: SourcePacket,
|
||||
proposals: tuple[ObjectProposal2D, ...],
|
||||
) -> tuple[ObstacleObservation, ...]:
|
||||
with self._lock:
|
||||
self._input_frames += 1
|
||||
baseline = self.base.associate(packet, proposals)
|
||||
started = time.perf_counter_ns()
|
||||
try:
|
||||
additive, candidate_points, voxel_count, peak_component_voxels = (
|
||||
self._build_additive_observations(packet, baseline)
|
||||
)
|
||||
result = (*baseline, *additive)
|
||||
validate_exclusive_point_ownership(result)
|
||||
except Exception:
|
||||
with self._lock:
|
||||
self._failed_frames += 1
|
||||
self._additive_core_duration_ns += max(
|
||||
0, time.perf_counter_ns() - started
|
||||
)
|
||||
raise
|
||||
with self._lock:
|
||||
self._completed_frames += 1
|
||||
self._frames_with_additions += bool(additive)
|
||||
self._candidate_points += candidate_points
|
||||
self._additive_observation_count += len(additive)
|
||||
self._additive_voxels += voxel_count
|
||||
self._peak_candidate_points = max(
|
||||
self._peak_candidate_points, candidate_points
|
||||
)
|
||||
self._peak_additive_observations = max(
|
||||
self._peak_additive_observations, len(additive)
|
||||
)
|
||||
self._peak_component_voxels = max(
|
||||
self._peak_component_voxels, peak_component_voxels
|
||||
)
|
||||
self._additive_core_duration_ns += max(
|
||||
0, time.perf_counter_ns() - started
|
||||
)
|
||||
return tuple(result)
|
||||
|
||||
def _build_additive_observations(
|
||||
self,
|
||||
packet: SourcePacket,
|
||||
baseline: tuple[ObstacleObservation, ...],
|
||||
) -> tuple[tuple[ObstacleObservation, ...], int, int, int]:
|
||||
frame = self.store.frame(packet)
|
||||
if frame is None or not frame.surface_valid:
|
||||
return (), 0, 0, 0
|
||||
step = self.store.point_step_candidates_for_frame(frame.frame_index)
|
||||
if step is None or step.shape != (frame.source_point_count,):
|
||||
raise M48LowStepOccupancyError("low-step point index space changed")
|
||||
claimed = {
|
||||
point_id for observation in baseline for point_id in observation.source_point_ids
|
||||
}
|
||||
candidate = np.flatnonzero(
|
||||
(step > 0) & (frame.point_class != POINT_OCCUPIED)
|
||||
).astype(np.int64)
|
||||
if claimed and candidate.size:
|
||||
candidate = candidate[
|
||||
np.fromiter(
|
||||
(int(value) not in claimed for value in candidate),
|
||||
dtype=np.bool_,
|
||||
count=int(candidate.size),
|
||||
)
|
||||
]
|
||||
if candidate.size:
|
||||
ranges = np.linalg.norm(
|
||||
frame.points_map[candidate] - frame.sensor_position_map,
|
||||
axis=1,
|
||||
)
|
||||
candidate = candidate[ranges <= self.profile.component.local_radius_m]
|
||||
candidate_count = int(candidate.size)
|
||||
if candidate_count > self.profile.component.maximum_candidate_points_per_frame:
|
||||
raise M48LowStepOccupancyError(
|
||||
"low-step candidate point capacity exceeded; dropping is forbidden"
|
||||
)
|
||||
components = _voxel_components(
|
||||
frame.points_map,
|
||||
candidate,
|
||||
self.profile.component,
|
||||
)
|
||||
qualified = tuple(
|
||||
item
|
||||
for item in components
|
||||
if item[0].size >= self.profile.component.minimum_points
|
||||
and item[1] >= self.profile.component.minimum_voxels
|
||||
)
|
||||
qualified = tuple(
|
||||
sorted(
|
||||
qualified,
|
||||
key=lambda item: (
|
||||
float(
|
||||
np.min(
|
||||
np.linalg.norm(
|
||||
frame.points_map[item[0]]
|
||||
- frame.sensor_position_map,
|
||||
axis=1,
|
||||
)
|
||||
)
|
||||
),
|
||||
int(item[0][0]),
|
||||
),
|
||||
)
|
||||
)
|
||||
if len(qualified) > self.profile.component.maximum_components_per_frame:
|
||||
raise M48LowStepOccupancyError(
|
||||
"low-step component capacity exceeded; dropping is forbidden"
|
||||
)
|
||||
observations: list[ObstacleObservation] = []
|
||||
voxel_count = 0
|
||||
peak_voxels = 0
|
||||
for component_index, (indices, cells) in enumerate(qualified):
|
||||
if cells > self.profile.component.maximum_cells_per_component:
|
||||
raise M48LowStepOccupancyError(
|
||||
"low-step component cell capacity exceeded; dropping is forbidden"
|
||||
)
|
||||
point_ids = tuple(sorted(int(value) for value in indices))
|
||||
points = frame.points_map[indices]
|
||||
centroid = np.median(points, axis=0)
|
||||
covariance = points.var(axis=0)
|
||||
nearest = float(
|
||||
np.min(np.linalg.norm(points - frame.sensor_position_map, axis=1))
|
||||
)
|
||||
observations.append(
|
||||
ObstacleObservation(
|
||||
observation_id=(
|
||||
f"{packet.envelope.frame_id}:low-step:{component_index}"
|
||||
),
|
||||
occupancy_key=(
|
||||
f"{packet.envelope.frame_id}:low-step:{component_index}"
|
||||
),
|
||||
source_id=packet.envelope.source_id,
|
||||
frame_id=packet.envelope.frame_id,
|
||||
evidence_time_ns=packet.envelope.timestamps.source_ns,
|
||||
basis=EvidenceBasis.LIDAR,
|
||||
currentness=EvidenceCurrentness.CURRENT,
|
||||
occupied_support=True,
|
||||
source_point_ids=point_ids,
|
||||
metric_geometry=MetricGeometry(
|
||||
coordinate_frame=self.store.profile.coordinate_frame,
|
||||
centroid_xyz_m=(
|
||||
float(centroid[0]),
|
||||
float(centroid[1]),
|
||||
float(centroid[2]),
|
||||
),
|
||||
range_m=nearest,
|
||||
covariance_diagonal_m2=(
|
||||
float(covariance[0]),
|
||||
float(covariance[1]),
|
||||
float(covariance[2]),
|
||||
),
|
||||
),
|
||||
proposal_ids=(),
|
||||
semantic_hint=None,
|
||||
reason_codes=(
|
||||
"additive-low-step-current-component",
|
||||
"occupied-only-never-free",
|
||||
),
|
||||
)
|
||||
)
|
||||
voxel_count += cells
|
||||
peak_voxels = max(peak_voxels, cells)
|
||||
return tuple(observations), candidate_count, voxel_count, peak_voxels
|
||||
|
||||
def snapshot(self) -> M48LowStepOccupancySnapshot:
|
||||
with self._lock:
|
||||
return M48LowStepOccupancySnapshot(
|
||||
base=self.base.snapshot(),
|
||||
input_frames=self._input_frames,
|
||||
completed_frames=self._completed_frames,
|
||||
failed_frames=self._failed_frames,
|
||||
frames_with_additions=self._frames_with_additions,
|
||||
candidate_point_count=self._candidate_points,
|
||||
additive_observation_count=self._additive_observation_count,
|
||||
additive_voxel_count=self._additive_voxels,
|
||||
peak_candidate_points_per_frame=self._peak_candidate_points,
|
||||
peak_additive_observations_per_frame=(
|
||||
self._peak_additive_observations
|
||||
),
|
||||
peak_voxels_per_component=self._peak_component_voxels,
|
||||
additive_core_duration_ns=self._additive_core_duration_ns,
|
||||
)
|
||||
|
||||
|
||||
def load_m48_low_step_occupancy_profile(
|
||||
path: Path,
|
||||
) -> M48LowStepOccupancyProfile:
|
||||
resolved = path.resolve(strict=True)
|
||||
if resolved.is_symlink() or not resolved.is_file():
|
||||
raise M48LowStepOccupancyError("low-step profile must be a regular file")
|
||||
raw = resolved.read_bytes()
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise M48LowStepOccupancyError("low-step profile JSON is invalid") from exc
|
||||
document = _object(value, "low-step profile")
|
||||
_exact_keys(
|
||||
document,
|
||||
{
|
||||
"schema_version",
|
||||
"profile_id",
|
||||
"provider_id",
|
||||
"base_geometry",
|
||||
"source",
|
||||
"componentization",
|
||||
"separation_expectations",
|
||||
"acceptance",
|
||||
"policy",
|
||||
"authority",
|
||||
},
|
||||
"low-step profile",
|
||||
)
|
||||
if (
|
||||
document["schema_version"] != M48_LOW_STEP_PROFILE_SCHEMA
|
||||
or document["provider_id"] != M48_LOW_STEP_PROVIDER_ID
|
||||
):
|
||||
raise M48LowStepOccupancyError("low-step profile identity changed")
|
||||
base = _object(document["base_geometry"], "base geometry")
|
||||
source = _object(document["source"], "low-step source")
|
||||
component = _object(document["componentization"], "componentization")
|
||||
acceptance = _object(document["acceptance"], "low-step acceptance")
|
||||
_exact_keys(base, {"profile_id", "sha256"}, "base geometry")
|
||||
_exact_keys(
|
||||
source,
|
||||
{
|
||||
"source_id",
|
||||
"session_id",
|
||||
"frame_count",
|
||||
"point_count",
|
||||
"local_surface_model_id",
|
||||
"local_surface_sha256",
|
||||
"m48r2_result_id",
|
||||
"m48r2_cases_sha256",
|
||||
},
|
||||
"low-step source",
|
||||
)
|
||||
_exact_keys(
|
||||
component,
|
||||
{
|
||||
"voxel_size_m",
|
||||
"neighbor_radius_cells",
|
||||
"minimum_points",
|
||||
"minimum_voxels",
|
||||
"local_radius_m",
|
||||
"maximum_candidate_points_per_frame",
|
||||
"maximum_cells_per_component",
|
||||
"maximum_components_per_frame",
|
||||
"exclude_baseline_occupied_points",
|
||||
"exclude_claimed_source_points",
|
||||
},
|
||||
"componentization",
|
||||
)
|
||||
_exact_keys(
|
||||
acceptance,
|
||||
{
|
||||
"expected_frames",
|
||||
"requested_source_rate_hz",
|
||||
"minimum_effective_world_state_fps",
|
||||
"maximum_world_state_completion_p95_ms",
|
||||
"maximum_geometry_stage_p95_ms",
|
||||
"maximum_geometry_stage_p99_ms",
|
||||
"maximum_fps_regression_fraction_vs_native_baseline",
|
||||
"maximum_world_state_p95_delta_ms_vs_native_baseline",
|
||||
"maximum_additive_component_mean_growth_fraction",
|
||||
"maximum_additive_cell_mean_growth_fraction",
|
||||
"maximum_capacity_drop_count",
|
||||
"minimum_critical_near_recall",
|
||||
"minimum_canonical_engineering_recall",
|
||||
"maximum_false_free_count",
|
||||
},
|
||||
"low-step acceptance",
|
||||
)
|
||||
if (
|
||||
component["exclude_baseline_occupied_points"] is not True
|
||||
or component["exclude_claimed_source_points"] is not True
|
||||
):
|
||||
raise M48LowStepOccupancyError("low-step point ownership policy changed")
|
||||
for key in (
|
||||
"expected_frames",
|
||||
"maximum_capacity_drop_count",
|
||||
"maximum_false_free_count",
|
||||
):
|
||||
item = acceptance.get(key)
|
||||
if not isinstance(item, int) or isinstance(item, bool) or item < 0:
|
||||
raise M48LowStepOccupancyError("low-step acceptance bounds are invalid")
|
||||
for key in (
|
||||
"requested_source_rate_hz",
|
||||
"minimum_effective_world_state_fps",
|
||||
"maximum_world_state_completion_p95_ms",
|
||||
"maximum_geometry_stage_p95_ms",
|
||||
"maximum_geometry_stage_p99_ms",
|
||||
"maximum_fps_regression_fraction_vs_native_baseline",
|
||||
"maximum_world_state_p95_delta_ms_vs_native_baseline",
|
||||
"maximum_additive_component_mean_growth_fraction",
|
||||
"maximum_additive_cell_mean_growth_fraction",
|
||||
"minimum_critical_near_recall",
|
||||
"minimum_canonical_engineering_recall",
|
||||
):
|
||||
if _number(acceptance, key) < 0.0:
|
||||
raise M48LowStepOccupancyError("low-step acceptance bounds are invalid")
|
||||
if (
|
||||
not _string(source, "m48r2_result_id").startswith(
|
||||
"m48-static-occupancy-qualification-"
|
||||
)
|
||||
or len(_string(source, "m48r2_result_id"))
|
||||
!= len("m48-static-occupancy-qualification-") + 64
|
||||
):
|
||||
raise M48LowStepOccupancyError("M4.8R2 result binding is invalid")
|
||||
_digest(source, "m48r2_cases_sha256")
|
||||
expected_policy = {
|
||||
"absence_of_points_means_free": False,
|
||||
"absence_of_camera_detection_means_free": False,
|
||||
"additive_only": True,
|
||||
"semantic_class_used": False,
|
||||
"ray_clearing_used": False,
|
||||
"planner_authoritative_free_space_claimed": False,
|
||||
}
|
||||
expected_authority = {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
if document["policy"] != expected_policy or document["authority"] != expected_authority:
|
||||
raise M48LowStepOccupancyError("low-step policy or authority changed")
|
||||
expectations_value = document["separation_expectations"]
|
||||
if not isinstance(expectations_value, list) or not expectations_value:
|
||||
raise M48LowStepOccupancyError("low-step separation expectations are missing")
|
||||
expectations: list[LowStepSeparationExpectation] = []
|
||||
for value in expectations_value:
|
||||
item = _object(value, "separation expectation")
|
||||
_exact_keys(
|
||||
item,
|
||||
{"anchor_id", "sequence", "expected_minimum_components", "interpretation"},
|
||||
"separation expectation",
|
||||
)
|
||||
expectations.append(
|
||||
LowStepSeparationExpectation(
|
||||
anchor_id=_string(item, "anchor_id"),
|
||||
sequence=_positive_integer(item, "sequence"),
|
||||
expected_minimum_components=_positive_integer(
|
||||
item, "expected_minimum_components"
|
||||
),
|
||||
interpretation=_string(item, "interpretation"),
|
||||
)
|
||||
)
|
||||
return M48LowStepOccupancyProfile(
|
||||
profile_id=_string(document, "profile_id"),
|
||||
provider_id=_string(document, "provider_id"),
|
||||
source_id=_string(source, "source_id"),
|
||||
session_id=_string(source, "session_id"),
|
||||
frame_count=_positive_integer(source, "frame_count"),
|
||||
point_count=_positive_integer(source, "point_count"),
|
||||
local_surface_model_id=_string(source, "local_surface_model_id"),
|
||||
local_surface_sha256=_digest(source, "local_surface_sha256"),
|
||||
base_geometry_profile_id=_string(base, "profile_id"),
|
||||
base_geometry_profile_sha256=_digest(base, "sha256"),
|
||||
component=LowStepComponentProfile(
|
||||
voxel_size_m=_number(component, "voxel_size_m"),
|
||||
neighbor_radius_cells=_positive_integer(
|
||||
component, "neighbor_radius_cells"
|
||||
),
|
||||
minimum_points=_positive_integer(component, "minimum_points"),
|
||||
minimum_voxels=_positive_integer(component, "minimum_voxels"),
|
||||
local_radius_m=_number(component, "local_radius_m"),
|
||||
maximum_candidate_points_per_frame=_positive_integer(
|
||||
component, "maximum_candidate_points_per_frame"
|
||||
),
|
||||
maximum_cells_per_component=_positive_integer(
|
||||
component, "maximum_cells_per_component"
|
||||
),
|
||||
maximum_components_per_frame=_positive_integer(
|
||||
component, "maximum_components_per_frame"
|
||||
),
|
||||
),
|
||||
separation_expectations=tuple(expectations),
|
||||
profile_sha256=hashlib.sha256(raw).hexdigest(),
|
||||
)
|
||||
|
||||
|
||||
def _voxel_components(
|
||||
points_map: npt.NDArray[np.float64],
|
||||
source_indices: IntArray,
|
||||
profile: LowStepComponentProfile,
|
||||
) -> tuple[tuple[IntArray, int], ...]:
|
||||
if source_indices.size == 0:
|
||||
return ()
|
||||
cells = np.floor(
|
||||
points_map[source_indices] / profile.voxel_size_m
|
||||
).astype(np.int64)
|
||||
cell_points: dict[tuple[int, int, int], list[int]] = {}
|
||||
for local_index, row in enumerate(cells):
|
||||
key = (int(row[0]), int(row[1]), int(row[2]))
|
||||
cell_points.setdefault(key, []).append(int(source_indices[local_index]))
|
||||
remaining = set(cell_points)
|
||||
radius = profile.neighbor_radius_cells
|
||||
neighbors = tuple(
|
||||
(dx, dy, dz)
|
||||
for dx in range(-radius, radius + 1)
|
||||
for dy in range(-radius, radius + 1)
|
||||
for dz in range(-radius, radius + 1)
|
||||
if dx or dy or dz
|
||||
)
|
||||
components: list[tuple[IntArray, int]] = []
|
||||
while remaining:
|
||||
seed = min(remaining)
|
||||
remaining.remove(seed)
|
||||
connected = [seed]
|
||||
queue = deque((seed,))
|
||||
while queue:
|
||||
cell = queue.popleft()
|
||||
for delta in neighbors:
|
||||
neighbor = (
|
||||
cell[0] + delta[0],
|
||||
cell[1] + delta[1],
|
||||
cell[2] + delta[2],
|
||||
)
|
||||
if neighbor not in remaining:
|
||||
continue
|
||||
remaining.remove(neighbor)
|
||||
connected.append(neighbor)
|
||||
queue.append(neighbor)
|
||||
indices = np.asarray(
|
||||
[point for cell in sorted(connected) for point in cell_points[cell]],
|
||||
dtype=np.int64,
|
||||
)
|
||||
components.append((indices, len(connected)))
|
||||
components.sort(key=lambda item: int(item[0][0]))
|
||||
return tuple(components)
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, object]:
|
||||
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
|
||||
raise M48LowStepOccupancyError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _exact_keys(value: dict[str, object], expected: set[str], label: str) -> None:
|
||||
if set(value) != expected:
|
||||
raise M48LowStepOccupancyError(f"{label} fields changed")
|
||||
|
||||
|
||||
def _string(value: dict[str, object], key: str) -> str:
|
||||
item = value.get(key)
|
||||
if not isinstance(item, str) or not item.strip():
|
||||
raise M48LowStepOccupancyError(f"{key} must be a non-empty string")
|
||||
return item
|
||||
|
||||
|
||||
def _positive_integer(value: dict[str, object], key: str) -> int:
|
||||
item = value.get(key)
|
||||
if not isinstance(item, int) or isinstance(item, bool) or item < 1:
|
||||
raise M48LowStepOccupancyError(f"{key} must be a positive integer")
|
||||
return item
|
||||
|
||||
|
||||
def _number(value: dict[str, object], key: str) -> float:
|
||||
item = value.get(key)
|
||||
if not isinstance(item, (int, float)) or isinstance(item, bool):
|
||||
raise M48LowStepOccupancyError(f"{key} must be numeric")
|
||||
result = float(item)
|
||||
if not math.isfinite(result):
|
||||
raise M48LowStepOccupancyError(f"{key} must be finite")
|
||||
return result
|
||||
|
||||
|
||||
def _digest(value: dict[str, object], key: str) -> str:
|
||||
item = _string(value, key)
|
||||
if len(item) != 64 or any(character not in "0123456789abcdef" for character in item):
|
||||
raise M48LowStepOccupancyError(f"{key} must be a SHA-256 digest")
|
||||
return item
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M48AdditiveLowStepGeometryProvider",
|
||||
"M48LowStepOccupancyError",
|
||||
"M48LowStepOccupancyProfile",
|
||||
"M48LowStepOccupancySnapshot",
|
||||
"M48_LOW_STEP_PROVIDER_ID",
|
||||
"load_m48_low_step_occupancy_profile",
|
||||
]
|
||||
@@ -26,6 +26,10 @@ from .geometry import (
|
||||
)
|
||||
from .graph import DeliveryEvidenceObserver, ReferencePerceptionGraphV2
|
||||
from .graph_contracts import DeliveredFrame, GraphRunMode
|
||||
from .m48_low_step_occupancy import (
|
||||
M48AdditiveLowStepGeometryProvider,
|
||||
load_m48_low_step_occupancy_profile,
|
||||
)
|
||||
from .motion import ClassIndependentMotionEstimator
|
||||
from .providers import (
|
||||
DetectorProvider,
|
||||
@@ -122,6 +126,7 @@ def build_m48s_reference_graph_runtime(
|
||||
decode_timing_observer: DecodeTimingObserver | None = None,
|
||||
source_pacing_observer: SourcePacingObserver | None = None,
|
||||
detector_timing_observer: DetectorTimingObserver | None = None,
|
||||
additive_low_step_profile: Path | None = None,
|
||||
maximum_frames: int | None = None,
|
||||
source_rate_hz: float | None = None,
|
||||
source_prefetch_capacity_frames: int = 64,
|
||||
@@ -133,7 +138,9 @@ def build_m48s_reference_graph_runtime(
|
||||
pinned_files = {
|
||||
ProviderRole.SOURCE: paths.baseline_profile,
|
||||
ProviderRole.DETECTOR: detector_profile,
|
||||
ProviderRole.GEOMETRY: paths.geometry_profile,
|
||||
ProviderRole.GEOMETRY: (
|
||||
additive_low_step_profile or paths.geometry_profile
|
||||
),
|
||||
ProviderRole.TEMPORAL: paths.temporal_motion_profile,
|
||||
ProviderRole.MOTION: paths.temporal_motion_profile,
|
||||
ProviderRole.ROLLING: paths.rolling_map_profile,
|
||||
@@ -144,6 +151,11 @@ def build_m48s_reference_graph_runtime(
|
||||
|
||||
load_m4_baseline(paths.baseline_profile)
|
||||
geometry_profile = load_geometry_profile(paths.geometry_profile)
|
||||
low_step_profile = (
|
||||
None
|
||||
if additive_low_step_profile is None
|
||||
else load_m48_low_step_occupancy_profile(additive_low_step_profile)
|
||||
)
|
||||
temporal_motion_profile = load_temporal_motion_profile(paths.temporal_motion_profile)
|
||||
rolling_map_profile = load_rolling_map_profile(paths.rolling_map_profile)
|
||||
threat_profile = load_replay_threat_profile(paths.threat_profile)
|
||||
@@ -204,11 +216,19 @@ def build_m48s_reference_graph_runtime(
|
||||
store,
|
||||
profile=threat_profile.body_frame,
|
||||
)
|
||||
geometry = (
|
||||
Ravnoves00GeometryAssociationProvider(store=store)
|
||||
if low_step_profile is None
|
||||
else M48AdditiveLowStepGeometryProvider(
|
||||
store=store,
|
||||
profile=low_step_profile,
|
||||
)
|
||||
)
|
||||
graph = ReferencePerceptionGraphV2(
|
||||
config=config,
|
||||
source=source,
|
||||
detector=detector,
|
||||
geometry=Ravnoves00GeometryAssociationProvider(store=store),
|
||||
geometry=geometry,
|
||||
temporal=BoundedSpatialTemporalProvider(
|
||||
point_resolver=store,
|
||||
profile=temporal_motion_profile,
|
||||
|
||||
Reference in New Issue
Block a user