feat(perception): qualify E31 source binding
This commit is contained in:
@@ -123,23 +123,44 @@ real authority.
|
|||||||
confidence `0.8730`. Frames 1213, 162 and 1823 are now explicit automatic
|
confidence `0.8730`. Frames 1213, 162 and 1823 are now explicit automatic
|
||||||
engineering outcomes; only geometry frames 2622 and 4147 remain in the
|
engineering outcomes; only geometry frames 2622 and 4147 remain in the
|
||||||
human queue.
|
human queue.
|
||||||
- [ ] Resolve only the resulting ambiguous/high-impact human-exception queue
|
- [x] Resolve the resulting ambiguous/high-impact human-exception queue and
|
||||||
and freeze the minimum correction set and cause distribution.
|
freeze the minimum correction set and cause distribution. The immutable
|
||||||
|
human generation is
|
||||||
|
`e30-review-generation-7982a882558d0be690b4c7092e328c080bfcbf52478a220452be7e887a588250`;
|
||||||
|
coverage is 2/2. Frame 2622 is `background-or-noise` and frame 4147 is
|
||||||
|
`object-present`.
|
||||||
|
- [x] Implement the E31 fail-closed source qualification contract over the
|
||||||
|
immutable E10→E30 chain. It accounts for all 4,489 source frames, audits
|
||||||
|
recorded host-arrival timing, sweeps nine offset hypotheses, verifies the
|
||||||
|
exact factory calibration identity and produces a source-scoped self-mask
|
||||||
|
profile without inferring firing time, sensor height or physical body
|
||||||
|
dimensions.
|
||||||
|
- [x] Accept the immutable diagnostic profile
|
||||||
|
`e31-source-qualification-b2460a5eb143688c7eea6821b2277e13aea79868abe81d83f7e78548c119159a`
|
||||||
|
for E32 binding. Zero offset retains support for 87/87 evidenced
|
||||||
|
correspondences. The semantic self-mask is admitted from eight source cases
|
||||||
|
with zero bbox-centre collateral; the generic geometry point mask is
|
||||||
|
rejected because it would erase accepted object evidence. Navigation,
|
||||||
|
safety and command authority remain false.
|
||||||
|
|
||||||
The current A3 AI-assisted engineering generation covers all 486 items and
|
The frozen A3 engineering and human generations cover all 486 items and
|
||||||
retains the exact A2 materialization/review-pack and 42-sheet evidence
|
retains the exact A2 materialization/review-pack and 42-sheet evidence
|
||||||
identities. It explicitly claims neither human ground truth nor navigation or
|
identities. It explicitly claims neither human ground truth nor navigation or
|
||||||
safety acceptance and retains `lab_published=false`. The dominant conflict
|
safety acceptance and retains `lab_published=false`. The dominant conflict
|
||||||
cause is detector placement on striped road/construction barriers rather than
|
cause is detector placement on striped road/construction barriers rather than
|
||||||
camera↔LiDAR registration; the `unknown` stratum is dominated by held
|
camera↔LiDAR registration; the `unknown` stratum is dominated by held
|
||||||
world-track time freshness; geometry-only contains both expected static scene
|
world-track time freshness; geometry-only contains both expected static scene
|
||||||
geometry and 21 visible missed class-bearing objects.
|
geometry and 21 visible missed class-bearing objects. Earlier generations and
|
||||||
|
drafts remain historical and are not rewritten or presented as the current
|
||||||
|
product workflow.
|
||||||
|
|
||||||
A3 is not complete until the two routed human exceptions are resolved and the
|
A3 and A4 are complete. E31 accepts only the recorded RAVNOVES00 diagnostic
|
||||||
minimum correction set is frozen. Earlier generations and drafts remain
|
binding and does not claim that host arrival is hardware firing time. The
|
||||||
historical and are not rewritten or presented as the current product workflow.
|
factory KB4 identity is exact, but a measured calibration-target residual and
|
||||||
A4 remains blocked. No perception threshold changes are allowed before the
|
physical body/mount dimensions are unavailable. The accepted profile is
|
||||||
exception decision is frozen.
|
therefore source-session scoped and cannot transfer to another mount. A5
|
||||||
|
`TrackGeometry v1` is the next critical-path implementation; E32 publication
|
||||||
|
must bind both that contract and the accepted E31 profile.
|
||||||
|
|
||||||
### A3 residual and human-exception policy
|
### A3 residual and human-exception policy
|
||||||
|
|
||||||
@@ -153,5 +174,6 @@ exception decision is frozen.
|
|||||||
accuracy, navigation or safety acceptance threshold.
|
accuracy, navigation or safety acceptance threshold.
|
||||||
- A repeated cause cluster or a high-impact case blocks the gate regardless of
|
- A repeated cause cluster or a high-impact case blocks the gate regardless of
|
||||||
percentage. The 1% budget cannot hide a systematic defect.
|
percentage. The 1% budget cannot hide a systematic defect.
|
||||||
- The current queue is 2 / 486 (`0.41%`) and therefore bounded, but A3 still
|
- The routed queue was 2 / 486 (`0.41%`) and is now fully resolved in the
|
||||||
requires those two recorded decisions before its correction set is frozen.
|
immutable human generation. No unresolved `insufficient-evidence` item
|
||||||
|
remains.
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# LAB E31 — source-time, calibration and self-mask qualification
|
||||||
|
|
||||||
|
Date: 2026-07-27
|
||||||
|
Status: accepted for diagnostic E32 binding; navigation, safety and command
|
||||||
|
authority are false
|
||||||
|
Immutable result:
|
||||||
|
`e31-source-qualification-b2460a5eb143688c7eea6821b2277e13aea79868abe81d83f7e78548c119159a`
|
||||||
|
|
||||||
|
## Decision under test
|
||||||
|
|
||||||
|
E31 tests whether the exact RAVNOVES00 source binding used by E29/E30 is
|
||||||
|
specific and stable enough to enter E32 full replay. It does not tune E29,
|
||||||
|
infer a raw LiDAR firing timestamp, invent a sensor height or promote the
|
||||||
|
result to navigation.
|
||||||
|
|
||||||
|
The gate is fail closed. It requires:
|
||||||
|
|
||||||
|
1. the immutable A3 engineering generation and complete human-exception
|
||||||
|
generation;
|
||||||
|
2. complete accounting of source-time bindings;
|
||||||
|
3. the exact factory KB4 calibration identity;
|
||||||
|
4. a predeclared offset sweep over evidenced camera↔LiDAR correspondences;
|
||||||
|
5. a source-evidenced self-mask decision that cannot silently erase accepted
|
||||||
|
object evidence.
|
||||||
|
|
||||||
|
## Immutable input chain
|
||||||
|
|
||||||
|
- Source pack:
|
||||||
|
`e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b`.
|
||||||
|
- Local-surface model:
|
||||||
|
`k1-local-surface-23762244c8bdb97de26fb721ac957d7a00bc9a63571ac4cfa4be19c4effc7d55`.
|
||||||
|
- E30 materialization:
|
||||||
|
`e30-materialization-841af926d8d28ab93538c46d8f31278a2234c4d1c12c7dc4dc296b249d59735a`.
|
||||||
|
- A3 engineering generation:
|
||||||
|
`e30-engineering-generation-62a4fea10dea9b77f69ceac1af5bf0e4928d9c7716083c22258a03670fe5bd4f`.
|
||||||
|
- A3 human generation:
|
||||||
|
`e30-review-generation-7982a882558d0be690b4c7092e328c080bfcbf52478a220452be7e887a588250`.
|
||||||
|
- Factory calibration content identity:
|
||||||
|
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`.
|
||||||
|
- Camera binding: `sensor.camera.right`, `camera_1`, KB4, `800×600`.
|
||||||
|
|
||||||
|
Every input result and artifact digest is verified before E31 runs. Existing
|
||||||
|
results are re-opened only when the complete content identity matches.
|
||||||
|
|
||||||
|
## Source-time audit
|
||||||
|
|
||||||
|
The recording has 4,489 source frames. LiDAR/pose bindings exist for 3,928
|
||||||
|
frames (`87.50%`); 561 unavailable frames remain explicitly unavailable.
|
||||||
|
|
||||||
|
| timing relation | absolute p50 | absolute p95 | maximum | declared gate |
|
||||||
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
|
| LiDAR ↔ camera | 25.303 ms | 70.789 ms | 99.574 ms | 100 ms |
|
||||||
|
| pose ↔ point cloud | 6.784 ms | 21.854 ms | 73.798 ms | 100 ms |
|
||||||
|
|
||||||
|
These are recorded host-monotonic arrival relations. They are not hardware
|
||||||
|
firing timestamps.
|
||||||
|
|
||||||
|
## Offset sensitivity
|
||||||
|
|
||||||
|
The correspondence set contains 87 A3 items where the projection was
|
||||||
|
`aligned`, point ownership was `object` and the E29 item was semantic. For each
|
||||||
|
hypothesis E31 selects the nearest available recorded cloud, projects it with
|
||||||
|
the camera-frame pose and evaluates occupied support inside the exact E29 bbox
|
||||||
|
inset (`0.03`).
|
||||||
|
|
||||||
|
| host-arrival hypothesis | evaluable | supported | supported fraction |
|
||||||
|
| ---: | ---: | ---: | ---: |
|
||||||
|
| -200 ms | 79 | 63 | 79.75% |
|
||||||
|
| -150 ms | 83 | 66 | 79.52% |
|
||||||
|
| -100 ms | 81 | 70 | 86.42% |
|
||||||
|
| -50 ms | 87 | 78 | 89.66% |
|
||||||
|
| 0 ms | 87 | 87 | 100.00% |
|
||||||
|
| +50 ms | 87 | 80 | 91.95% |
|
||||||
|
| +100 ms | 83 | 69 | 83.13% |
|
||||||
|
| +150 ms | 84 | 71 | 84.52% |
|
||||||
|
| +200 ms | 83 | 69 | 83.13% |
|
||||||
|
|
||||||
|
Zero offset is retained. Its support deficit relative to the best hypothesis is
|
||||||
|
zero. The p95 bbox support-centroid residual is `0.3947` bbox diagonals. This is
|
||||||
|
a diagnostic correspondence residual, not a measured calibration-target
|
||||||
|
residual.
|
||||||
|
|
||||||
|
## Calibration and self-mask result
|
||||||
|
|
||||||
|
The loaded factory calibration exactly matches the source-pack identity. The
|
||||||
|
camera-from-LiDAR rotation determinant is `1.00000037`, the maximum
|
||||||
|
orthonormal residual is `5.30e-7`, and translation norm is `0.1005 m`.
|
||||||
|
|
||||||
|
Eight A3 `self_points` person cases support a source-session semantic exclusion
|
||||||
|
rectangle:
|
||||||
|
|
||||||
|
```text
|
||||||
|
normalized xyxy = [0.191196, 0.730990, 0.625038, 0.992935]
|
||||||
|
application = person bbox centre inside rectangle
|
||||||
|
observed collateral = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
This mask is bound to the source session and detector rule. It is not a
|
||||||
|
transferable physical vehicle mask.
|
||||||
|
|
||||||
|
The two geometry `self_points` cases cannot support a generic projected point
|
||||||
|
mask. Their union would erase 69 selected points from one accepted object and
|
||||||
|
22 from another. E31 therefore rejects that mask and retains the two exact A3
|
||||||
|
correction item identities instead.
|
||||||
|
|
||||||
|
Physical body dimensions and a body-frame mount transform are unavailable.
|
||||||
|
E31 records both as absent; it does not substitute a guessed height.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
All ten predeclared requirements pass. The result status is
|
||||||
|
`accepted-diagnostic-source-profile` and `eligible_for_e32=true`.
|
||||||
|
|
||||||
|
Acceptance means only:
|
||||||
|
|
||||||
|
- E32 may bind this exact source session, factory calibration, zero
|
||||||
|
host-arrival offset and source-scoped semantic mask;
|
||||||
|
- unavailable source evidence remains unavailable;
|
||||||
|
- the unsafe generic geometry mask remains disabled;
|
||||||
|
- the result has no command, navigation or safety authority.
|
||||||
|
|
||||||
|
It does not establish transfer to another physical mount. That remains an E36
|
||||||
|
question with a second eligible real source.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src .venv/bin/python \
|
||||||
|
experiments/perception/run_e31_source_qualification.py \
|
||||||
|
--source-pack .runtime/compute-experiments/e10/lidar-packs/e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b \
|
||||||
|
--local-surface .runtime/compute-experiments/k1-local-surface-v1/models/k1-local-surface-23762244c8bdb97de26fb721ac957d7a00bc9a63571ac4cfa4be19c4effc7d55 \
|
||||||
|
--calibration .runtime/mission-core/evidence/sessions/private/device-calibration/20260720T105432Z_k1_factory_calibration_2e0e51ae5485 \
|
||||||
|
--materialization .runtime/compute-experiments/e30/materializations/e30-materialization-841af926d8d28ab93538c46d8f31278a2234c4d1c12c7dc4dc296b249d59735a \
|
||||||
|
--engineering-generation .runtime/compute-experiments/e30/engineering-generations/e30-engineering-generation-62a4fea10dea9b77f69ceac1af5bf0e4928d9c7716083c22258a03670fe5bd4f \
|
||||||
|
--human-generation .runtime/compute-experiments/e30/human-review-generations/e30-review-generation-7982a882558d0be690b4c7092e328c080bfcbf52478a220452be7e887a588250
|
||||||
|
```
|
||||||
|
|
||||||
|
The immutable result contains:
|
||||||
|
|
||||||
|
- `qualification-report.json`;
|
||||||
|
- `binding-profile.json`;
|
||||||
|
- `offset-sweep.json`;
|
||||||
|
- 87 per-item correspondence score rows;
|
||||||
|
- a digest-bound manifest.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build the immutable LAB E31 source qualification profile."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from k1link.compute.e31_source_qualification import (
|
||||||
|
build_e31_source_qualification,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--source-pack", type=Path, required=True)
|
||||||
|
parser.add_argument("--local-surface", type=Path, required=True)
|
||||||
|
parser.add_argument("--calibration", type=Path, required=True)
|
||||||
|
parser.add_argument("--materialization", type=Path, required=True)
|
||||||
|
parser.add_argument("--engineering-generation", type=Path, required=True)
|
||||||
|
parser.add_argument("--human-generation", type=Path, required=True)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-root",
|
||||||
|
type=Path,
|
||||||
|
default=Path(".runtime/compute-experiments/e31/source-qualifications"),
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
result = build_e31_source_qualification(
|
||||||
|
source_pack_root=args.source_pack,
|
||||||
|
local_surface_root=args.local_surface,
|
||||||
|
calibration_snapshot_root=args.calibration,
|
||||||
|
materialization_root=args.materialization,
|
||||||
|
engineering_generation_root=args.engineering_generation,
|
||||||
|
human_generation_root=args.human_generation,
|
||||||
|
output_root=args.output_root,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"result_id": result.result_id,
|
||||||
|
"result_root": str(result.result_root),
|
||||||
|
"status": result.report["status"],
|
||||||
|
"eligible_for_e32": result.report["eligible_for_e32"],
|
||||||
|
"requirements": result.report["requirements"],
|
||||||
|
"authority": result.report["authority"],
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -7,6 +7,16 @@ from .annotation_workspace import (
|
|||||||
prepare_annotation_workspace,
|
prepare_annotation_workspace,
|
||||||
validate_annotation_workspace,
|
validate_annotation_workspace,
|
||||||
)
|
)
|
||||||
|
from .e31_source_qualification import (
|
||||||
|
DEFAULT_E31_SOURCE_QUALIFICATION_PROFILE,
|
||||||
|
E31_BINDING_PROFILE_SCHEMA,
|
||||||
|
E31_SOURCE_QUALIFICATION_REPORT_SCHEMA,
|
||||||
|
E31_SOURCE_QUALIFICATION_SCHEMA,
|
||||||
|
E31SourceQualification,
|
||||||
|
E31SourceQualificationError,
|
||||||
|
E31SourceQualificationProfile,
|
||||||
|
build_e31_source_qualification,
|
||||||
|
)
|
||||||
from .evaluation_pack import (
|
from .evaluation_pack import (
|
||||||
ANNOTATION_CONTRACT_SCHEMA,
|
ANNOTATION_CONTRACT_SCHEMA,
|
||||||
EVALUATION_PACK_SCHEMA,
|
EVALUATION_PACK_SCHEMA,
|
||||||
@@ -210,6 +220,13 @@ __all__ = [
|
|||||||
"COMPUTE_JOB_SCHEMA",
|
"COMPUTE_JOB_SCHEMA",
|
||||||
"DEFAULT_QUALIFICATION_FRAME_COUNT",
|
"DEFAULT_QUALIFICATION_FRAME_COUNT",
|
||||||
"DEFAULT_K1_LOCAL_SURFACE_PROFILE",
|
"DEFAULT_K1_LOCAL_SURFACE_PROFILE",
|
||||||
|
"DEFAULT_E31_SOURCE_QUALIFICATION_PROFILE",
|
||||||
|
"E31_BINDING_PROFILE_SCHEMA",
|
||||||
|
"E31_SOURCE_QUALIFICATION_REPORT_SCHEMA",
|
||||||
|
"E31_SOURCE_QUALIFICATION_SCHEMA",
|
||||||
|
"E31SourceQualification",
|
||||||
|
"E31SourceQualificationError",
|
||||||
|
"E31SourceQualificationProfile",
|
||||||
"EVALUATION_PACK_SCHEMA",
|
"EVALUATION_PACK_SCHEMA",
|
||||||
"EvaluationFrameRequest",
|
"EvaluationFrameRequest",
|
||||||
"EvaluationPackFrame",
|
"EvaluationPackFrame",
|
||||||
@@ -320,6 +337,7 @@ __all__ = [
|
|||||||
"prepare_recorded_qualification_slice",
|
"prepare_recorded_qualification_slice",
|
||||||
"assess_lidar_profile",
|
"assess_lidar_profile",
|
||||||
"build_lidar_replay_pack_v2",
|
"build_lidar_replay_pack_v2",
|
||||||
|
"build_e31_source_qualification",
|
||||||
"build_lidar_ground_annotation_template",
|
"build_lidar_ground_annotation_template",
|
||||||
"build_lidar_ground_benchmark",
|
"build_lidar_ground_benchmark",
|
||||||
"build_k1_local_surface",
|
"build_k1_local_surface",
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
|
|||||||
project_map_points_kb4,
|
project_map_points_kb4,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .lidar_field_review import E10LidarFieldSource
|
|
||||||
from .lidar_local_surface import K1LocalSurfaceV1
|
|
||||||
from .e30_camera_evidence import (
|
from .e30_camera_evidence import (
|
||||||
E30CameraEvidenceSource,
|
E30CameraEvidenceSource,
|
||||||
materialize_e30_camera_frames,
|
materialize_e30_camera_frames,
|
||||||
open_e30_camera_evidence_source,
|
open_e30_camera_evidence_source,
|
||||||
)
|
)
|
||||||
|
from .lidar_field_review import E10LidarFieldSource
|
||||||
|
from .lidar_local_surface import K1LocalSurfaceV1
|
||||||
from .semantic_geometry_fusion import (
|
from .semantic_geometry_fusion import (
|
||||||
CAMERA_GEOMETRY_FUSION_SCHEMA,
|
CAMERA_GEOMETRY_FUSION_SCHEMA,
|
||||||
CameraGeometryFusionProfile,
|
CameraGeometryFusionProfile,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,12 +9,12 @@ from fastapi import Path as ApiPath
|
|||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
from k1link.compute.e30_human_review import (
|
from k1link.compute.e30_human_review import (
|
||||||
|
E30ExceptionDisposition,
|
||||||
E30HumanReviewConflictError,
|
E30HumanReviewConflictError,
|
||||||
E30HumanReviewIntegrityError,
|
E30HumanReviewIntegrityError,
|
||||||
E30HumanReviewNotFoundError,
|
E30HumanReviewNotFoundError,
|
||||||
E30HumanReviewStore,
|
E30HumanReviewStore,
|
||||||
E30HumanReviewValidationError,
|
E30HumanReviewValidationError,
|
||||||
E30ExceptionDisposition,
|
|
||||||
E30ReviewSubject,
|
E30ReviewSubject,
|
||||||
E30ReviewSubstrate,
|
E30ReviewSubstrate,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from k1link.compute.e31_source_qualification import (
|
||||||
|
E31SourceQualificationError,
|
||||||
|
E31SourceQualificationProfile,
|
||||||
|
_E30Chain,
|
||||||
|
_offset_sweep,
|
||||||
|
_self_mask_report,
|
||||||
|
)
|
||||||
|
from k1link.compute.lidar_local_surface import POINT_OCCUPIED
|
||||||
|
|
||||||
|
|
||||||
|
class _SweepSource:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.arrays = {
|
||||||
|
"session_seconds": np.asarray([0.0, 0.1, 0.2], dtype=np.float64),
|
||||||
|
"sample_available": np.ones(3, dtype=np.bool_),
|
||||||
|
"cloud_offsets": np.asarray([0, 2, 4, 6], dtype=np.int64),
|
||||||
|
"cloud_points_map": np.asarray(
|
||||||
|
[
|
||||||
|
[1.0, 0.0, 2.0],
|
||||||
|
[1.1, 0.0, 2.0],
|
||||||
|
[0.0, 0.0, 2.0],
|
||||||
|
[0.1, 0.0, 2.0],
|
||||||
|
[-1.0, 0.0, 2.0],
|
||||||
|
[-1.1, 0.0, 2.0],
|
||||||
|
],
|
||||||
|
dtype=np.float32,
|
||||||
|
),
|
||||||
|
"pose_positions_map": np.zeros((3, 3), dtype=np.float64),
|
||||||
|
"pose_quaternions_map_from_lidar": np.asarray(
|
||||||
|
[[0.0, 0.0, 0.0, 1.0]] * 3,
|
||||||
|
dtype=np.float64,
|
||||||
|
),
|
||||||
|
"intrinsic_fx_fy_cx_cy": np.asarray(
|
||||||
|
[100.0, 100.0, 50.0, 50.0],
|
||||||
|
dtype=np.float64,
|
||||||
|
),
|
||||||
|
"distortion_kb4": np.zeros(4, dtype=np.float64),
|
||||||
|
"t_camera_from_lidar": np.eye(4, dtype=np.float64),
|
||||||
|
}
|
||||||
|
self.identity: dict[str, Any] = {
|
||||||
|
"source_id": "sensor.camera.right",
|
||||||
|
"camera_slot": "camera_1",
|
||||||
|
"projection": {"width": 100, "height": 100},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _SweepSurface:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.arrays = {
|
||||||
|
"point_class": np.full(6, POINT_OCCUPIED, dtype=np.uint8),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact(path: Path) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"path": path.name,
|
||||||
|
"byte_length": path.stat().st_size,
|
||||||
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _projection_artifact(
|
||||||
|
root: Path,
|
||||||
|
name: str,
|
||||||
|
pixels: list[list[float]],
|
||||||
|
) -> dict[str, object]:
|
||||||
|
path = root / f"{name}.npz"
|
||||||
|
np.savez(
|
||||||
|
path,
|
||||||
|
projected_pixels_xy=np.asarray(pixels, dtype=np.float32),
|
||||||
|
projected_selected_mask=np.ones(len(pixels), dtype=np.uint8),
|
||||||
|
)
|
||||||
|
return _artifact(path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_rejects_ambiguous_offset_hypotheses() -> None:
|
||||||
|
with pytest.raises(E31SourceQualificationError):
|
||||||
|
E31SourceQualificationProfile(offset_hypotheses_ms=(0, -50, 50))
|
||||||
|
with pytest.raises(E31SourceQualificationError):
|
||||||
|
E31SourceQualificationProfile(offset_hypotheses_ms=(-50, 50))
|
||||||
|
|
||||||
|
|
||||||
|
def test_offset_sweep_keeps_evidenced_zero_binding() -> None:
|
||||||
|
source = _SweepSource()
|
||||||
|
surface = _SweepSurface()
|
||||||
|
profile = E31SourceQualificationProfile(
|
||||||
|
offset_hypotheses_ms=(-100, 0, 100),
|
||||||
|
minimum_correspondence_items=1,
|
||||||
|
)
|
||||||
|
item = {
|
||||||
|
"item_id": "item-1",
|
||||||
|
"review_key": "semantic:1:1",
|
||||||
|
"evidence_binding": {"frame_index": 1},
|
||||||
|
"e29_snapshot": {"bbox_xyxy": [40.0, 40.0, 60.0, 60.0]},
|
||||||
|
}
|
||||||
|
|
||||||
|
sweep, rows = _offset_sweep(
|
||||||
|
source=source, # type: ignore[arg-type]
|
||||||
|
surface=surface, # type: ignore[arg-type]
|
||||||
|
items=(item,),
|
||||||
|
profile=profile,
|
||||||
|
bbox_inset_fraction=0.03,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sweep["selected_offset_ms"] == 0
|
||||||
|
assert sweep["baseline_supported_fraction"] == 1.0
|
||||||
|
assert sweep["best_supported_fraction"] == 1.0
|
||||||
|
assert sweep["baseline_support_deficit_fraction"] == 0.0
|
||||||
|
assert [row["supported_count"] for row in sweep["hypotheses"]] == [0, 1, 0]
|
||||||
|
assert [score["candidate_frame_index"] for score in rows[0]["scores"]] == [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_self_mask_admits_only_non_colliding_semantic_rule(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
items: list[dict[str, Any]] = []
|
||||||
|
decisions: list[dict[str, Any]] = []
|
||||||
|
for index in range(4):
|
||||||
|
item = {
|
||||||
|
"item_id": f"semantic-self-{index}",
|
||||||
|
"e29_snapshot": {
|
||||||
|
"label": "person",
|
||||||
|
"bbox_xyxy": [
|
||||||
|
20.0 + index,
|
||||||
|
75.0,
|
||||||
|
40.0 + index,
|
||||||
|
99.0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
items.append(item)
|
||||||
|
decisions.append(
|
||||||
|
{
|
||||||
|
"item_id": item["item_id"],
|
||||||
|
"cause_code": "self_points",
|
||||||
|
"point_ownership": "self",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
geometry_self = {
|
||||||
|
"item_id": "geometry-self",
|
||||||
|
"e29_snapshot": {},
|
||||||
|
"artifact": _projection_artifact(
|
||||||
|
tmp_path,
|
||||||
|
"geometry-self",
|
||||||
|
[[20.0, 20.0], [30.0, 30.0]],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
accepted_object = {
|
||||||
|
"item_id": "accepted-object",
|
||||||
|
"e29_snapshot": {},
|
||||||
|
"artifact": _projection_artifact(
|
||||||
|
tmp_path,
|
||||||
|
"accepted-object",
|
||||||
|
[[25.0, 25.0], [80.0, 80.0]],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
accepted_person = {
|
||||||
|
"item_id": "accepted-person",
|
||||||
|
"e29_snapshot": {
|
||||||
|
"label": "person",
|
||||||
|
"bbox_xyxy": [70.0, 50.0, 90.0, 90.0],
|
||||||
|
},
|
||||||
|
"artifact": _projection_artifact(
|
||||||
|
tmp_path,
|
||||||
|
"accepted-person",
|
||||||
|
[[80.0, 80.0]],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
items.extend([geometry_self, accepted_object, accepted_person])
|
||||||
|
decisions.extend(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_id": geometry_self["item_id"],
|
||||||
|
"cause_code": "self_points",
|
||||||
|
"point_ownership": "self",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": accepted_object["item_id"],
|
||||||
|
"cause_code": "none",
|
||||||
|
"point_ownership": "object",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": accepted_person["item_id"],
|
||||||
|
"cause_code": "none",
|
||||||
|
"point_ownership": "object",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
chain = _E30Chain(
|
||||||
|
materialization_manifest={
|
||||||
|
"identity": {
|
||||||
|
"projection": {
|
||||||
|
"width": 100,
|
||||||
|
"height": 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items=tuple(items),
|
||||||
|
engineering_manifest={},
|
||||||
|
decisions=tuple(decisions),
|
||||||
|
exceptions=(),
|
||||||
|
human_manifest={},
|
||||||
|
human_decisions=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
report = _self_mask_report(
|
||||||
|
materialization_root=tmp_path,
|
||||||
|
chain=chain,
|
||||||
|
profile=E31SourceQualificationProfile(
|
||||||
|
minimum_semantic_self_samples=4,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert report["semantic_mask"]["status"] == "admitted"
|
||||||
|
assert report["semantic_mask"]["application_rule"] == "bbox-center-inside-rectangle"
|
||||||
|
assert report["semantic_mask"]["collateral_item_count"] == 0
|
||||||
|
assert report["geometry_point_mask"]["status"] == "rejected"
|
||||||
|
assert report["geometry_point_mask"]["collateral"] == [
|
||||||
|
{
|
||||||
|
"item_id": "accepted-object",
|
||||||
|
"masked_selected_point_count": 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert report["exact_correction_item_ids"] == ["geometry-self"]
|
||||||
Reference in New Issue
Block a user