Skip to content

test: Tier 7 tools/corpus overhaul (post-#784) — replace SMART emulator with sphere-model corpus + runner #795

@SlyWombat

Description

@SlyWombat

Scope

Tier 7 of the post-#784 test-suite overhaul. Covers offline tools and the synthetic corpus consumed by the cal-pipeline emulator:

All four are tied to the deletion-track SMART pipeline. The replacement is a NEW sphere-model corpus that exercises (profile, home_anchor, homeSecondary, rotation, target_xyz) → expected (panDmx16, tiltDmx16). This child issue specs the corpus shape; it does NOT write the corpus content.

Architectural ground rules

  • The acceptance-gate concern survives — every PR that touches the aim/cal pipeline must pass an offline corpus run before merge (per CLAUDE.md ## Cal-pipeline change checklist (#733)).
  • The DRIVER (tools/emulate_smart_pipeline.py) and the CORPUS (tests/fixtures/cal/corpus.json) both delete and are replaced.
  • The replacement targets aim/sphere.py::AimSphere directly. No SMART probe loop, no camera, no DMX wire.

Inventory

KEEP

None — every existing tool/corpus in this tier is on the deletion track.

REWRITE

None — the spec is a fresh write rather than a rewrite of existing content.

DELETE

File LOC Concern Confirmed deletion target PR gate Replacement
tools/emulate_smart_pipeline.py 279 SMART pipeline emulator: coverage_math.coverage_polygon, working_area, sample_grid, solve_dmx_per_degree, angles_to_dmx against the cal corpus. YES — entire SMART pipeline + coverage_math IK funcs delete. PR-7 NEW tools/emulate_aim_sphere.py (or tools/sphere_corpus_runner.py) — see ADD.
tests/fixtures/cal/corpus.json n/a Synthetic-fixture corpus: per-case {profile_id, fixture_xyz, fixture_rotation, home_anchor, secondary, target_xyz} → expected SMART invariants. YES PR-7 NEW tests/fixtures/aim/sphere_corpus.json — see ADD.
tools/post_cal_confirm.py (audit) Tool that drives a cal'd mover + ArUco markers and returns CONFIRMED / OFF_TARGET / NO_DETECTION / BEHIND_CAMERA / NO_PROJECTION verdicts (#682-FF). LIKELY YES — consumed by SMART pipeline post-cal flow. Audit needed. PR-7 If repurposed for post-Save-Home wizard verification, REWRITE the tool (re-target inputs to homePanDmx16 / homeTiltDmx16 / homeSecondary / aimSphere). If purely SMART: DELETE.
tools/cal_trace/ (replay tool) NDJSON cal-trace replay (#686). Consumed by tests/test_cal_trace.py + tests/test_cal_trace_v2.py. LIKELY YES PR-7 If repurposed for wizard diagnostics: REWRITE. Otherwise DELETE.

ADD — replacement architecture

New artifact Purpose Suggested location PR gate Spec
Sphere-model corpus Replace tests/fixtures/cal/corpus.json. Drives aim/sphere.py::AimSphere directly. tests/fixtures/aim/sphere_corpus.json PR-7 See "Corpus shape" below.
Sphere-model corpus runner Replace tools/emulate_smart_pipeline.py. Loads corpus, instantiates AimSphere per case, asserts expectations. tools/sphere_corpus_runner.py PR-7 See "Runner shape" below.
Weekly regression wrapper Replace tests/regression/test_smart_pipeline_emulator.py with the equivalent thin shell around the new runner. tests/regression/test_sphere_corpus.py PR-7 Subprocess invocation, exit code propagation, same as the old wrapper.
Cal-pipeline checklist update CLAUDE.md ## Cal-pipeline change checklist (#733) mentions tools/emulate_smart_pipeline.py — update to point at the new runner. docs change in CLAUDE.md PR-7 Trivial.

Corpus shape

{
  "schemaVersion": 1,
  "description": "Sphere-model corpus — replaces tests/fixtures/cal/corpus.json post-#784 PR-7.",
  "cases": [
    {
      "name": "fid17-back-right-marker",
      "fixture": {
        "id": 17,
        "x": 600, "y": 0, "z": 1760,
        "rotation": [0, 0, 0],
        "homePanDmx16": 44364,
        "homeTiltDmx16": 0,
        "homeSecondary": {
          "panMovedDirection": "right",
          "tiltMovedDirection": "down",
          "panOffsetDmx16": 10922,
          "tiltOffsetDmx16": 32768
        }
      },
      "profile": { "id": "movinghead-150w-12ch", "panRange": 540, "tiltRange": 180 },
      "target_xyz": [500, 2280, 0],
      "expected": {
        "panDmx16_within": [44560, 44760],
        "tiltDmx16_within": [13550, 13860],
        "az_deg": -2.51,
        "el_deg": -37.64,
        "tolerance": { "pan_dmx16": 200, "tilt_dmx16": 250, "deg": 2.0 }
      }
    },
    {
      "name": "upright-fixture-multi-valued-prefer-A",
      "...": "see notes below"
    }
  ]
}

Each case carries:

  • The minimum fixture record needed to construct AimSphere (x/y/z/rotation/homePanDmx16/homeTiltDmx16/homeSecondary).
  • The minimum profile record (id/panRange/tiltRange plus optional dmxToMechanical).
  • The aim input — either target_xyz (drives aim_xyz) or azDeg/elDeg (drives aim_direction).
  • Expected outputs with explicit tolerances (DMX cell quantization is ~2°, so DMX tolerance scales with panRange/tiltRange and a 2° cell).

Required cases (initial battery)

The corpus must contain AT LEAST these cases to lock down the bug surface:

  1. 6 ArUco-marker walkthrough on fid 17 — same input as the fix(#784 PR-3): aim/sphere.py — 2°-cell precomputed lookup, bilinear bracketing-cell blend, clipped-target nearest-row fallback #785 round-2 QA. All 6 markers, expected DMX from the operator-confirmed reality (Bug 1 + 2 regression).
  2. Round-trip XYZ ↔ angular — for every reachable target, aim_xyz(target_xyz) and aim_direction(stage_az, stage_el) byte-equivalent (fix(#784 PR-3): aim/sphere.py — 2°-cell precomputed lookup, bilinear bracketing-cell blend, clipped-target nearest-row fallback #785 Bug 5).
  3. Idempotence — two calls to aim_xyz(target_xyz) with identical inputs return identical DMX (fix(#784 PR-3): aim/sphere.py — 2°-cell precomputed lookup, bilinear bracketing-cell blend, clipped-target nearest-row fallback #785 Bug 3 — current_pose plumbing).
  4. Degenerate targetaim_xyz(fixture_xyz) returns None (fix(#784 PR-3): aim/sphere.py — 2°-cell precomputed lookup, bilinear bracketing-cell blend, clipped-target nearest-row fallback #785 Bug 4).
  5. Multi-valued cell with prefer="A" / prefer="B" — distinct branches, deterministic.
  6. Multi-valued cell with current_pose — branch locks to current pose for prefer="closest".
  7. Inverted mount via rotation = [0, 180, 0] — same world target → mirrored DMX (the post-feat: SMART cal + mover-aim — single canonical IK, per-probe contract, lamp-on contract (consolidates #746, #747, #749, #760, #779) #780 P1 invariant).
  8. homeSecondary.panMovedDirection = "right" + offset > 0 — sign derivation: pan_sign = -1 (per fix(#784 PR-3): aim/sphere.py — 2°-cell precomputed lookup, bilinear bracketing-cell blend, clipped-target nearest-row fallback #785 sphere-cloud finding).
  9. homeSecondary.tiltMovedDirection = "down" + offset > 0 — sign derivation: tilt_sign = -1.
  10. Missing homeSecondaryAimSphere construction raises "fixture <id> has no homeSecondary direction calls + offsets".
  11. Below-horizon target on a tilt-down fixture — reachable; sphere returns DMX > home.
  12. Above-horizon target on a tilt-down fixture — unreachable; sphere returns None or clipped.
  13. All 4 corners of a stage's reachable polygon — all reachable, DMX inside [0, 65535] for both axes.
  14. Profile.panRange = 360° (single-valued) and panRange = 540° (multi-valued) cases — corpus includes both.
  15. Fixture rotation [10, 0, 45] (non-trivial pitch + yaw) — round-trip stays consistent.

Runner shape

# tools/sphere_corpus_runner.py
"""Sphere-model corpus runner — #784 PR-7 acceptance gate.

Replaces tools/emulate_smart_pipeline.py. Loads the case battery
from tests/fixtures/aim/sphere_corpus.json and asserts
AimSphere outputs match the per-case expectations (within
documented tolerances).

Exit codes:
  0 — every case passes its expectations.
  1 — at least one case violates an expectation.

Run: python tools/sphere_corpus_runner.py [--verbose]
CI: tests/regression/test_sphere_corpus.py wraps this.
"""

The runner instantiates AimSphere(fixture_stub, profile_stub, step=128) per case and exercises aim_xyz / aim_direction against the case expectations. Pure offline — no Flask, no camera, no DMX wire.

Existing-tools tier audit

Tool Status Action
tools/devgui/server.py KEEP Tier 8 — separate child issue.
tools/cal_trace/ (if exists) DELETE-or-REWRITE See above.
tools/post_cal_confirm.py DELETE-or-REWRITE See above.
tools/docs/build.py (manual builder) KEEP Manual-section docs sync via #662.

PR sequencing

  • DELETE rows + ADD rows all land in PR-7. The new corpus + runner replace the old corpus + emulator in the same PR; the regression-suite wrapper updates in the same PR.
  • CLAUDE.md ## Cal-pipeline change checklist updates with the new runner path.

Acceptance

  • python tools/sphere_corpus_runner.py --verbose exits 0 against tests/fixtures/aim/sphere_corpus.json.
  • python tests/regression/test_sphere_corpus.py exits 0 (subprocess wrapper).
  • The 15 case categories above are all populated in the corpus (with at least one case each).
  • CLAUDE.md updated to reference the new runner path.
  • tools/emulate_smart_pipeline.py and tests/fixtures/cal/corpus.json deleted.

Open questions for human review

  1. Does tools/post_cal_confirm.py get repurposed for Save-Home wizard verification, or deletes? Need an operator decision; affects whether tests/regression/test_post_cal_confirm.py survives (Tier 4 cross-reference).
  2. Same question for tools/cal_trace/ (replay tool).
  3. Does the corpus runner need to exercise the HTTP /api/mover/<fid>/aim route, or only the in-process AimSphere class? Spec above defaults to in-process (cheaper, no Flask spin-up). HTTP variant could be a regression-tier ADD instead.
  4. Is step=128 the right default for the corpus, or should the corpus declare per-case step? Default is fine for most cases; flag if a case needs step=32 for tight tolerance.

Cross-references

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions