From 3e86724169477ddd4632d5805f5c14bd9c8b7db0 Mon Sep 17 00:00:00 2001 From: Kirtan Date: Mon, 7 Sep 2026 09:50:07 +0530 Subject: [PATCH 1/5] docs(readme): redesign FSOC repository landing page for SIH Non-functional, judge-facing repository polish only -- no algorithm, model, controller, or measured behavior changed. Rewrites README.md from a chronological "Step 1 already implemented -> Step 2 ..." development log into a judge-facing landing page: problem statement, what FSOC does (SEE -> ESTIMATE -> PREDICT -> CORRECT), a Mermaid architecture diagram that explicitly separates the simulation environment from a labeled "not built, not claimed" future hardware interface, a measured-results table (with deliberate wording: "~99.5% reduction in severe outliers," never "99.5% accuracy"), a real (not fabricated) demo image plus a documented manual- capture TODO for a Mission Control screen recording, verified quick-start commands, and Known Limitations / Hardware Boundary sections carried forward verbatim from docs/SIH_MVP_FREEZE.md's safe-claims list. Moves the detailed Step 1-11 chronological build log (exact numbers, test counts, implementation notes) out of README.md into a new docs/DEVELOPMENT_HISTORY.md, preserved verbatim -- no engineering fact lost, just relocated so a judge doesn't have to scroll past 11 build-step headers to reach architecture/results. Archives four stale root-level starter-kit artifacts (KIT_MANIFEST.md, MIGRATION_FROM_PYTHON.md, FILES.txt, VALIDATION.txt -- all from the original Python-to-C++20 conversion, fully superseded by current docs) into docs/archive/ via `git mv`, each with a one-line "archived, here's why, see X for the current version" header. Retitles CHECKLIST.md from "48-Hour MVP Checklist" to reflect that it now covers Stage 2-4 AI perception and P0-v2 state estimation too. Verified before committing: golden-demo commands (normal/clutter/static --mode hybrid --tracker) re-run and match documented output exactly; no prohibited claim (real hardware, embedded performance, "clutter solved," "44.9% eliminated," "99.5% accuracy") appears anywhere in the new text. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014h1TXJD8THZH4NbMqzrx97 --- CHECKLIST.md | 6 +- README.md | 625 ++++++++---------- docs/DEVELOPMENT_HISTORY.md | 259 ++++++++ FILES.txt => docs/archive/FILES.txt | 4 + .../archive/KIT_MANIFEST.md | 5 + .../archive/MIGRATION_FROM_PYTHON.md | 3 + VALIDATION.txt => docs/archive/VALIDATION.txt | 4 + 7 files changed, 541 insertions(+), 365 deletions(-) create mode 100644 docs/DEVELOPMENT_HISTORY.md rename FILES.txt => docs/archive/FILES.txt (94%) rename KIT_MANIFEST.md => docs/archive/KIT_MANIFEST.md (67%) rename MIGRATION_FROM_PYTHON.md => docs/archive/MIGRATION_FROM_PYTHON.md (84%) rename VALIDATION.txt => docs/archive/VALIDATION.txt (98%) diff --git a/CHECKLIST.md b/CHECKLIST.md index b5ff314..34cd75a 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -1,4 +1,8 @@ -# 48-Hour MVP Checklist — C++20 +# FSOC Engineering Checklist — C++20 + +(Originally the "48-Hour MVP Checklist" for the initial baseline sprint; retitled as +the project grew through Stage 2-4 AI perception and P0-v2 state estimation. Content +below is a chronological, `[x]`-per-milestone engineering log, not a to-do list.) - [x] CMake/C++20 skeleton - [x] Frozen coordinate convention diff --git a/README.md b/README.md index de5aa0e..a184ef3 100644 --- a/README.md +++ b/README.md @@ -1,380 +1,268 @@ -# SIH26169 — FSOC Virtual Camera Tracking MVP (C++20) - -Engineering starter kit for **AI-Based Virtual Camera Tracking System for Coarse Alignment of Mobile Free Space Optical Communication (FSOC) Terminals**. - -This repository treats the challenge as a **closed-loop guidance, tracking, and control problem**: - -`Environment -> Camera -> Beacon measurement -> Estimation -> Prediction -> Control -> Pan/Tilt actuation -> Observation` - -## Language decision - -The project baseline is now **modern C++20**. There is no Python package, virtual environment, pip install, `pyproject.toml`, NumPy, or PyVista dependency in the core project — i.e. in the **runtime control loop**. `tools/ai/` is a separate, offline, one-time model-training toolchain (PyTorch → ONNX export, `tools/ai/README.md`) that produces the committed `models/tiny_beacon_net.onnx`; it is never imported, run, or required by the C++ runtime, which loads that ONNX file through OpenCV-DNN (see "AI Perception" below). - -For the first 48-hour MVP: -- Core math/physics/control: C++20 -- Build: CMake + Ninja -- Pixel simulation/tracking visualization: OpenCV C++ (introduced after the math gate) -- Step-1 vector math: dependency-free to keep the foundation auditable -- Later UKF/MPC phase: add Eigen when matrix-heavy estimation/control begins - -## Step 1 already implemented - -- 3D world convention -- pan/tilt camera basis -- pinhole projection -- finite camera FOV -- actuator velocity saturation -- tilt mechanical limits -- ideal pointing angles for diagnostics only -- terminal-only smoke test -- 12 unit checks using CTest, with no external test framework - -## Step 2 implemented — target trajectory engine - -- `TargetState` = world `position_m` + `velocity_mps` (SI, double precision) -- `Trajectory` abstract interface: pure `state_at(double time_s)`, no owned clock -- stationary, linear constant-velocity (signed), and sinusoidal trajectories -- sinusoidal velocity is the exact analytic derivative; frequency in Hz (`omega = 2*pi*f`) -- deliberate input validation via `std::invalid_argument` (non-finite / negative time, - non-finite params, negative frequency/amplitude) -- `step2_trajectory_smoke` + deterministic analytic CTest suite (`fsoc_step2_tests`) -- no coupling to camera / perception / control - -## Step 3 implemented — observation / measurement / tracking-error contracts - -- strongly typed layers kept distinct: `TargetState` (truth) → `CameraObservation` / - `Projection` (exact projection) → `BeaconDetection` (image estimate) → `TrackingError` - (controller-facing) -- `ObservationStatus` = `Visible` / `OutsideFieldOfView` / `BehindCamera`; - `observe_beacon()` reuses `PanTiltCamera::project()` — no duplicated projection math -- frozen image convention: origin top-left, `+x_px` right, `+y_px` down; - centre `cx = W/2.0`, `cy = H/2.0` owned by the camera -- frozen sign convention: pixel `error_x>0` = RIGHT, `error_y>0` = BELOW; - angular `pan_rad>0` = command pan right, `tilt_rad>0` = command tilt up -- `compute_tracking_error(std::optional, PanTiltCamera)` — `optional` - in / out, non-finite centroid rejected, reuses `pixel_error_to_angles` -- target-lost = empty `std::optional` only (no `(-1,-1)` / NaN / zero sentinels) -- `step3_observation_smoke` (machine-checks the critical (400,180) scenario) + - `fsoc_step3_tests` (all four quadrants, pinhole match, regressions) -- no OpenCV, no detector algorithm, no controller - -## Step 4 implemented — synthetic virtual-camera image renderer - -- first OpenCV use, isolated in a separate `fsoc_render` library; `fsoc_core` and all - pure-math headers stay OpenCV-free -- `SyntheticCameraRenderer::render(const CameraObservation&) -> cv::Mat` (`CV_8UC1`) -- uniform dark background (default 5 counts) + analytic 2-D Gaussian beacon - (peak 255, `sigma` in **pixels**), clamped to `[0,255]` -- **true sub-pixel** beacon centre — the fractional `ImagePoint` is never rounded before - the Gaussian is evaluated, so a weighted centroid recovers it -- edge-safe: the Gaussian is rasterised in a window clipped to the image -- `OutsideFieldOfView` / `BehindCamera` → background-only frame (no fake beacon) -- no sensor noise this step; the same observation renders byte-identical frames -- `step4_renderer_smoke` writes `generated/*.png` headlessly + `fsoc_step4_tests` -- CMake `find_package(OpenCV)` auto-detected (`FSOC_ENABLE_OPENCV=AUTO|ON|OFF`) - -## Step 5 implemented — baseline beacon detector - -- new `fsoc_perception` library (`fsoc::core` + OpenCV core/imgproc); **does not depend on - `fsoc_render`** — `BeaconDetector::detect(const cv::Mat&)` consumes pixels only, never - `TargetState` / trajectory / `CameraObservation` / the projected `ImagePoint` -- transparent pipeline: threshold (`pixel >= threshold_intensity`, default 64) → - 8-connected components → reject `area < min_bright_pixels` → pick the component with the - greatest integrated signal (ties: lowest label) → intensity-weighted centroid -- centroid weight `= (pixel − threshold) + 1` (no assumed background); recovers the - Gaussian's sub-pixel centre to **≈ 0.02 px** on clean interior frames (gate 0.15) -- found → `std::optional` (Step-3 type); not found → `std::nullopt` - (no `(-1,-1)` / NaN / zero sentinel); no fabricated confidence -- rejects empty / non-`CV_8UC1` frames with `std::invalid_argument` -- perception chain verified: renderer → detector → `compute_tracking_error` reproduces - RIGHT+ABOVE → pan > 0, tilt > 0 -- `step5_detector_smoke` (headless, `std::chrono` timing for curiosity) + `fsoc_step5_tests` - -## Step 6 implemented — pan/tilt PID controller - -- new `fsoc_control` library depending on **`fsoc::core` only** (via `fsoc/tracking_error.hpp`); - **OpenCV-free** — links no OpenCV / `fsoc_render` / `fsoc_perception`, builds without OpenCV -- `PIDController::update(const TrackingError&, double dt_s) -> ControlCommand` — angular - error (radians) in, pan/tilt **rate** (rad/s) out; never absolute angles, never touches - `PanTiltCamera` -- two independent axes, standard discrete PID `u = kp·e + ki·I + kd·D` on `angular.pan_rad` - / `angular.tilt_rad`; derivative forced to 0 on the first update after construction/`reset()` -- anti-windup: integral hard-clamped to ±`integral_limit` + conditional integration; output - clamped to ±`output_limit_rad_s` -- `reset()` clears integrals / previous errors / first-sample flags; `zero_control_command()` - helper for the runner's target-loss path -- rejects non-finite / ≤0 `dt_s` and non-finite `TrackingError` with `std::invalid_argument` - (state untouched on throw); invalid config rejected at construction -- sign preserved: `e > 0` (RIGHT / ABOVE) → command > 0 (PAN RIGHT / TILT UP) -- default gains are **untuned placeholders** (tuned in Step 7) -- `step6_pid_smoke` (5 scenarios + toy scalar-plant sanity) + `fsoc_step6_tests` - -## Step 7 implemented — closed-loop tracking simulation - -- new `fsoc_simulation` library (links `fsoc::core` + `render` + `perception` + `control`) - — the **one** intentional integration layer; owns the clock, fixed timestep, subsystem - call order, target-loss policy, and camera stepping, and **no** domain math -- `SimulationRunner::step()` runs one fixed timestep in this order: `trajectory.state_at(t)` - → `observe_beacon` → `renderer.render` → `detector.detect(cv::Mat)` → - `compute_tracking_error(detection, camera)` → `pid.update` (or loss policy) → - `camera.step` → record `SimulationStepResult` → `t += dt` -- fixed `dt = 0.02 s` (50 Hz); **never wall-clock**; same config + trajectory → - bit-identical result sequence -- **pixel-only feedback:** control is driven solely by the detected centroid; - `TargetState` / `observation.image_point_px` / exact `Projection` feed only the labelled - diagnostic fields and truth-vs-measurement scoring — proven by - `test_control_follows_detected_not_truth` -- **target-loss policy:** no detection → `pid.reset()` + zero command + camera holds (no - search); the loop resumes from reset if the target drifts back into the FOV -- `SimulationRunnerConfig::validate()` rejects PID output limit > camera actuator rate and - renderer/camera dimension mismatch -- empirically-tuned MVP baseline PID **kp = 12, ki = 0, kd = 0** (P-dominant on the - integrator plant — not claimed optimal); results: static acquisition **4.13° → 0.0° in - ~0.34 s**, sinusoidal (±12.4°) RMS **0.55°** at 100 % detection, open-loop → closed-loop - detection **57 % → 100 %** and RMS **6.45° → 0.55°** -- `step7_closed_loop_smoke` (static / sinusoidal / open-vs-closed) + `fsoc_step7_tests` - -## Step 8 implemented — telemetry + benchmarking - -- new `fsoc_telemetry` library — an **observer**: consumes `SimulationStepResult`, never - calls back into the loop. Running with vs without telemetry yields a bit-identical - `SimulationStepResult` sequence (mandatory non-interference test) -- `TelemetryRecord` — 27 flat, unit-suffixed, JSON-mappable fields; unavailable - measurements are `std::optional` in memory (**no `-1` / NaN / `N/A` sentinel**) and empty - fields in CSV; `TrackingState { Tracking, TargetLost }` -- `CsvTelemetryLogger` — synchronous `std::ofstream`, one flushed line per record, no - threads / async / external CSV dependency; writes `generated/step8_*.csv` (git-ignored) -- `BenchmarkMetrics` / `compute_benchmark_metrics` — detection %, RMS/mean/max/final/**P95** - angular error, mean/RMS/max pixel error, mean detection error, command/pan/tilt - saturation fractions, mean|abs|+peak applied rates, wall time + processing FPS. Error - metrics over frames with a `TrackingError`; percentile = nearest-rank `ceil(0.95·N)-1` -- **wall clock vs simulation clock:** physics stays on the fixed `dt = 0.02 s` (50 Hz); - `processing_fps = frames / wall_time` is measured with `std::chrono` around the step loop - only (~4700 FPS ≈ 90× real time) and never feeds the sim dt -- `step8_telemetry_smoke` runs the 4 benchmark scenarios, exports CSVs, prints the - comparison table (Static P95 0.17°, Sinusoidal-closed P95 0.79° vs Sinusoidal-open P95 - 9.81°) + `fsoc_step8_tests` - -## Step 9 implemented — engineering camera-view visualization - -- new `fsoc_visualization` library — an **observer**: `TrackingVisualizer::annotate()` takes - the perception `CV_8UC1` frame **by const& (never modified)** and returns a **new - `CV_8UC3` BGR** display frame. The control path keeps running on the original unannotated - image; overlay pixels can never reach the detector -- `SimulationRunner` / `SimulationStepResult` **not changed** — the base frame is - reconstructed from `result.observation` via a deterministic `SyntheticCameraRenderer` -- overlays: centre crosshair (from frame geometry, not hardcoded 320/240), detection marker - at `telemetry.detected_*`, centre→detected error vector (shrinks to zero on convergence), - `TRACKING` / `TARGET LOST`, `VISIBLE` vs `DETECTED`, SIM/FRAME, PAN/TILT (deg), ANG ERR - (deg), ERR PX, CMD rates (deg/s) with amber `RATE LIMIT` from the Step-8 `*_saturated` - flags -- colours: green = tracking, red = lost, amber = saturation, grey = neutral; optional - `DETECT ERR` and `TRUTH` square marker are **off by default** -- headless: PNG per selected frame (required) + optional best-effort `cv::VideoWriter` MP4 - (graceful `false` when no codec / no `videoio`); output → `generated/step9/` (git-ignored) -- mandatory non-interference test passed (500 frames with/without annotation → identical - `SimulationStepResult` sequence); static story visually verified (frame 0: 4.13° / long - vector / 30°/s + `RATE LIMIT` → final: beacon on crosshair / 0.00° / 0°/s) -- `step9_visualization_smoke` (static / sinusoidal / target-lost) + `fsoc_step9_tests` - -## Step 10 implemented — baseline acceptance / validation suite - -- new `fsoc_validation` library (links `fsoc::simulation` + `fsoc::telemetry` + - `fsoc::visualization`) — an **evaluation layer**. It runs the *existing* v1 system across - seven named deterministic scenarios and checks acceptance gates; it implements **no** - trajectory / detector / PID / renderer / camera math and never controls the loop or - changes an algorithm to improve a number -- **gates are frozen up front** in `docs/16_BASELINE_ACCEPTANCE.md` — physically justified, - documented, **not** derived from the run being scored. Baseline PID stays **kp = 12, - ki = 0, kd = 0** -- scenarios: **A** Static Acquisition · **B** Slow Linear Tracking · **C** Sinusoidal - Tracking · **D** Near-FOV-Edge Acquisition · **E** Actuator Saturation · **F** Target - Loss and Re-entry · **G** Open Loop vs Closed Loop -- per-scenario global checks: finite values (no NaN/Inf), monotonic timestamps, fixed dt, - command rate ≤ PID limit, applied rate ≤ actuator limit, target-loss semantics, and a - **deterministic-replay** check (a second independent run is bit-identical) -- a **mandatory failure-check test** injects an impossible gate and tightens a real - threshold past its actual value and confirms the evaluator then reports FAIL — it is not - an always-green harness -- `step10_validation_smoke` prints a judge-friendly table, writes CSV + annotated PNG - evidence and `generated/step10/VALIDATION_REPORT.md` (values generated from the run, not - hardcoded), and ends with `STEP 10 BASELINE ACCEPTANCE: PASS` — **7 / 7 scenarios pass** - (static 4.13° → 0.00°; sinusoidal RMS 0.55°; open→closed detection 57.4 % → 100 %, RMS - 6.45° → 0.55°, ×11.8). `fsoc_step10_tests` green (10 checks) -- generated evidence → `generated/step10/` (git-ignored); the canonical gate definitions - live in `docs/16_BASELINE_ACCEPTANCE.md` (committed). The **`v1_baseline` tag is created - and pushed** to `origin`, pointing at the merged Step‑10 baseline (`20c028c`); that - validated baseline is **frozen** - -## Step 11 implemented — demo freeze + frontend data contract prep - -- new `fsoc_demo_support` library (links `fsoc::simulation` + `fsoc::telemetry`) — an - **additive** presentation layer built *after* the frozen baseline. `v1_baseline` (tag, - pushed to `origin`) stays put; this step changes **no** validated algorithm. Geometry / - camera / trajectory / renderer / detector / `TrackingError` / PID law / PID gains / - `SimulationRunner` order / Step-10 gates are all untouched. -- **`DemoScenario`** — `static` · `sinusoidal` · `loss` · `open` · `closed`, selected by a - clean token (`parse_demo_scenario`). Each reuses the validated Step-10 trajectory/config - **verbatim** (no retuning). `open` and `closed` share identical trajectory parameters — - only `control_enabled` differs. -- **`DemoSnapshot`** — a per-frame view model for a future UI, built **only** from - `SimulationStepResult` + `TelemetryRecord` + `CameraConfig` by `make_demo_snapshot()`. It - never participates in control. Optionals are `std::nullopt` when the target is lost — no - sentinels. Fields + the future JSON shape are frozen in - `docs/18_FRONTEND_DATA_CONTRACT.md`. -- **Units** — core stays radians / rad·s⁻¹ / m / m·s⁻¹ / px. Degrees appear only via - `to_degrees(const DemoSnapshot&)` at the UI boundary; core physics units are unchanged. -- **`DemoSession`** — deterministic packaging of one scenario: owns a heap `Trajectory` - (constructed before, so it outlives the `SimulationRunner`) + the runner + the telemetry - conversion. `DemoRunState { Ready, Running, Paused, Finished }` is application state, - distinct from `TrackingState`; a **paused `step()` does not advance simulation time**. - `reset()` reproduces a bit-identical run. -- **`fsoc_demo` CLI** — `./build/debug/fsoc_demo [--duration s] [--csv path] - [--quiet]` / `--help`. Per-frame status lines + an end-of-run summary (detection %, RMS / - P95 / max angular error, lost frames) computed by the existing Step-8 `BenchmarkMetrics`. -- **non-interference** (mandatory) — for all 5 scenarios a bare `SimulationRunner` and the - `DemoSession` produce field-identical `SimulationStepResult` sequences. `fsoc_step11_tests` - green (23 checks). Demo numbers match Step 10 exactly. -- reproducible: `make demo` (or `scripts/run_baseline_demo.sh`) runs the Step-10 validation - + the static & sinusoidal demos + the Step-9 visualization evidence. See - `docs/17_DEMO_FREEZE.md` for the teammate-Mac checklist. - -## AI Perception (V2) implemented — real trained model, real C++ inference, safety-gated - -Additive, post-`v1_baseline` work on `feat/ai-perception`. The frozen classical -baseline above is **unchanged** by any of this — see `docs/19_AI_PERCEPTION_ARCHITECTURE.md` -and ADR-015/016/017/018 in `DECISIONS.md` for the full design history. +# FSOC + +### Autonomous closed-loop coarse alignment for mobile Free-Space Optical Communication terminals + +**Smart India Hackathon 2026 · SIH26169** + +[![Build & Test](https://github.com/ThatKJ/FSOC/actions/workflows/ci.yml/badge.svg)](https://github.com/ThatKJ/FSOC/actions/workflows/ci.yml) +![C++20](https://img.shields.io/badge/C%2B%2B-20-blue) + +FSOC detects an optical beacon through a virtual pan/tilt camera, fuses classical and +neural (CNN) perception, estimates short-term target motion, and automatically commands +pan/tilt corrections to keep the beacon aligned — a full closed-loop guidance / tracking +/ control stack, implemented and measured end-to-end in a deterministic C++20 +simulation. + +## The problem + +Free-Space Optical (FSO) communication links move data at high bandwidth over narrow, +highly directional laser beams — and that narrowness is the weakness: on a **mobile** +terminal (vehicle, vessel, aircraft), platform motion and vibration constantly knock the +beam off target. Before a fine-tracking stage can lock on, something has to keep the +remote terminal inside that fine-tracker's narrow capture range in the first place. +That's **coarse alignment** — SIH26169's problem statement — a real perception + control +problem, not a scripted vision demo. + +## What FSOC does + +``` +SEE → ESTIMATE → PREDICT → CORRECT +``` + +- **SEE** — a synthetic pan/tilt camera observes a moving beacon; a classical + threshold/centroid detector *and* a real trained CNN (`TinyBeaconNet`) each + independently propose a centroid. +- **ESTIMATE** — a Safe Hybrid fusion policy combines them, rejecting disagreement + outright rather than guessing which one is right. +- **PREDICT** — a minimal alpha-beta state estimator tracks position/velocity and + bridges brief detection gaps instead of losing the target on every blink. +- **CORRECT** — a PID controller turns the fused, estimated position into pan/tilt rate + commands, actuated by a saturating virtual gimbal. + +Every stage above is real, tested C++20 code, not a scripted or pre-recorded demo. + +## Architecture + +```mermaid +flowchart TD + subgraph SIM["SIMULATION ENVIRONMENT — this repository, today"] + T["Trajectory / Environment"] --> C["Synthetic Camera
(SyntheticCameraRenderer)"] + C --> Dz["Image Disturbance
(optional: noise / clutter / occlusion)"] + Dz --> CL["Classical Detector
(threshold + centroid)"] + Dz --> AI["TinyBeaconNet
(CNN, ONNX, C++ inference)"] + CL --> F["Safe Hybrid Fusion
(resolve_perception, ADR-018)"] + AI --> F + F --> TR["TargetTracker
(alpha-beta + temporal gate, ADR-019)"] + TR --> S["Control Safety
(is_safe_to_steer)"] + S --> P["PID Controller"] + P --> G["Pan / Tilt Actuator
(PanTiltCamera)"] + G -.next frame.-> C + end + subgraph HW["FUTURE HARDWARE INTERFACE — not built, not claimed"] + RC["Real camera / frame grabber"] -. would replace .-> C + RG["Real servo / gimbal driver"] -. would replace .-> G + end +``` + +No physical camera, beacon, or pan/tilt hardware exists anywhere in this repository +today — see **Hardware boundary** below. The interfaces above (`FrameSource` / +`Detector` / `Controller` / `PanTiltCamera`) are deliberately swappable +(`docs/09_FUTURE_ARCHITECTURE.md`) so that boundary can move later without touching the +detector, PID, or `SimulationRunner` step order. + +## Measured results + +Every number below comes from a committed, deterministic tool in this repository — +none is estimated or hand-picked. Full methodology and raw evidence: +`docs/MVP_METRICS.md`, `docs/MVP_ABLATION.md`, `docs/SIH_MVP_FREEZE.md`. + +| | | +|---|---| +| Step-10 baseline acceptance | **7 / 7 PASS** | +| C++ test suites (`ctest`) | **17 / 17** (100%) | +| Frontend end-to-end tests (Playwright) | **20 / 20** | +| Severe (>50px) closed-loop outliers — Classical | 2,240 | +| Severe closed-loop outliers — Classical + Tracker | 12 (**↓ ~99.5%**) | +| Severe closed-loop outliers — Hybrid | 1,808 | +| Severe closed-loop outliers — Hybrid + Tracker (V2) | 9 (**↓ ~99.5%**) | +| Full-step latency, Hybrid + Tracker, P95 | **~1.2 ms**, vs. the 20 ms / 50 Hz budget | +| Telemetry pipeline | 42-column real CSV export, header-driven | + +**On wording**, deliberately: this is a *"~99.5% reduction in severe (>50px) closed-loop +pointing outliers, measured in the deterministic simulation evaluation"* — not "99.5% +accuracy," and not a claim that clutter false-locking is solved in general. See **Known +limitations** below for exactly what this does and does not fix. + +## Working demo + +

+ FSOC tracking view — Step-9 visualizer output, static acquisition scenario +

+ +*Real Step-9 visualizer output (the same renderer/overlay code the frontend and CLI use) +— not a mockup.* + +Five self-contained, one-command, reproducible conditions (`docs/MVP_GOLDEN_DEMO.md` has +the full 15-step judge walkthrough with narration): + +```bash +./build/debug/fsoc_demo normal # calm baseline +./build/debug/fsoc_demo noise # ordinary sensor noise, handled unaided +./build/debug/fsoc_demo clutter # the false-lock problem, live, honestly framed +./build/debug/fsoc_demo occlusion # short-gap prediction bridging +./build/debug/fsoc_demo reacquisition # full Lost -> fresh-reacquire state-machine cycle +``` + +> **Demo Preview (manual capture pending)** — the images above are real, but a short +> screen capture of the live Mission Control session (ENGINE mode, `clutter` or +> `reacquisition` preset, the "STATE ESTIMATOR (P0-v2)" panel visible) would be the +> single highest-value visual to add here. To capture it: `cd frontend && npm run dev`, +> switch the source toggle to **ENGINE**, select a scenario, and record the browser tab +> (a 10-15s GIF or MP4 of the telemetry rail updating live is enough — no editing +> needed). Drop it at `frontend/public/demo/mission-control.gif` and reference it here. + +## AI / Hybrid perception + +Additive, post-`v1_baseline` work — the frozen classical baseline is **unchanged** by +any of this (`docs/19_AI_PERCEPTION_ARCHITECTURE.md`, `DECISIONS.md` ADR-015/016/017/018). - **A real trained model, not a stub.** `TinyBeaconNet` (27,282 parameters, a small - fully-convolutional heatmap network — not a downloaded/pretrained backbone) is trained - on a deterministic, seeded synthetic dataset (`fsoc_ai_datagen`, domain-randomized - noise/blur/clutter/distractors), exported to ONNX (opset 12), and loaded natively in - C++ via OpenCV-DNN (`AiBeaconDetector`, `include/fsoc/ai_beacon_detector.hpp`). No - Python is in the runtime path. Python↔ONNX Runtime↔C++ numeric parity is measured, not - assumed: centroid agreement is **1.54e-7 px** on this machine (`fsoc_ai_beacon_detector_tests`). -- **Safe Hybrid fusion (ADR-018), not a naive confidence blend.** `resolve_perception()` + fully-convolutional heatmap network) is trained on a deterministic, seeded synthetic + dataset, exported to ONNX, and loaded natively in C++ via OpenCV-DNN + (`AiBeaconDetector`) — no Python in the runtime path. Python↔ONNX Runtime↔C++ numeric + parity is measured: centroid agreement **1.54e-7 px** on this machine. +- **Safe Hybrid fusion (ADR-018), not a confidence blend.** `resolve_perception()` implements a frozen decision table: classical+AI agreement (≤8.0 px) accepts the - classical centroid; classical-only accepts classical; **AI-only and any classical/AI - disagreement both reject unconditionally** — no confidence override, no averaging, - no "trust whichever is brighter." Stage-2 evidence (`DECISIONS.md` ADR-018) showed AI - confidence does not separate correct from wrong detections, so a lone high-confidence - AI candidate is deliberately never trusted alone. -- **Measured, unflattering-where-true evaluation**, not marketing numbers. - `stage4_evaluation` (`docs/21_AI_STAGE4_EVALUATION_PROTOCOL.md`) scores Classical vs. - AI vs. Hybrid across 11 deterministic degraded scenarios. Headline finding: Hybrid - alone reduces severe closed-loop wrong-lock outliers by ~20% vs. Classical, but does - **not** fix Classical's own bright-clutter false-lock vulnerability (44.9% aggregate - common-frame false-positive rate) unaided — full numbers and the caveats discovered - while producing them are in `docs/MVP_METRICS.md`. -- **Reachable from the actual demo, not just from unit tests.** - `fsoc_demo --mode classical|ai|hybrid` runs the same validated closed loop - with AI/Hybrid perception live; falls back to classical with a visible warning if the - ONNX model can't be loaded (Phase-7-style failure handling, not a crash). Mission - Control's telemetry rail shows the live mode/source/AI-confidence/rejection-reason. - -## State estimation + clutter mitigation (P0-v2) — measured, not just implemented - -Additive, post-Stage-4 work, still on `feat/ai-perception`. Closes the two gaps the AI -Perception section above states plainly (no motion filter; Hybrid alone doesn't fix -Classical's clutter false-lock): `fsoc::TargetTracker`, a minimal alpha-beta (g-h) state -estimator with a temporal-consistency gate — **not** a Kalman/UKF, a deliberate choice -(`include/fsoc/target_tracker.hpp`). Additive and default-off (`tracker_enabled = false` -/ `fsoc_demo --tracker`); every seam is proven bit-identical when disabled by a -dedicated regression test. + classical centroid; classical-only accepts classical; **AI-only and any + classical/AI disagreement both reject unconditionally** — no averaging, no "trust + whichever is brighter." +- **Measured, unflattering-where-true evaluation.** The frozen Stage-4 protocol + (`docs/21_AI_STAGE4_EVALUATION_PROTOCOL.md`) scores Classical vs. AI vs. Hybrid across + 11 deterministic degraded scenarios. Hybrid alone reduces severe closed-loop outliers + ~20% vs. Classical, but does **not** fix Classical's clutter false-lock unaided — that + required the state estimator below. +- Reachable from the real demo: `fsoc_demo --mode classical|ai|hybrid`, + with a loud fallback to Classical if the ONNX model can't load. + +## State estimation + prediction + +`fsoc::TargetTracker` — a minimal **alpha-beta (g-h)** state estimator with a temporal- +consistency gate, deliberately **not** a Kalman/UKF (`include/fsoc/target_tracker.hpp`). +Additive and default-off (`tracker_enabled = false` / `fsoc_demo --tracker`); every seam +is proven bit-identical to the pre-tracker behavior when disabled. - **Root cause found and fixed, not guessed.** Classical's clutter vulnerability traced to a bad-acquisition mechanism: 3 consecutive detections confirmed a track even when - they disagreed spatially. Fixed in the estimator's acquisition logic - (`DECISIONS.md` ADR-019). -- **Measured mitigation** (`docs/MVP_ABLATION.md`, `stage4_tracker_ablation`, full frozen - Stage-4 protocol, 22,000 frames/config): severe (>50px) closed-loop outliers fall from - 2,240 (Classical) / 1,808 (Hybrid) to 12 / 9 — a **99.5% reduction** — at a real, - disclosed coverage cost (~20 points). The intrinsic 44.9% single-frame FPR is - unchanged (unfixable without touching the frozen classical detector algorithm). + they disagreed spatially. Fixed in the estimator's acquisition logic (`DECISIONS.md` + ADR-019). +- **Measured mitigation**, full frozen Stage-4 protocol (22,000 frames/config): severe + outliers fall from 2,240 (Classical) / 1,808 (Hybrid) to 12 / 9 — a **~99.5% + reduction** — at a real, disclosed coverage cost (~20 points). The intrinsic 44.9% + single-frame false-positive rate is unchanged (unfixable without touching the frozen + classical detector algorithm). - **A real, disclosed limit, not hidden**: a *temporally coherent* (smoothly moving) - distractor defeats this mitigation completely (`docs/MVP_ABLATION.md §6`, - `mvp_dynamic_scenarios`) — the gate rejects spatially/temporally incoherent - candidates, not any adversarial one. -- **5 named, deterministic demo presets** — `fsoc_demo normal|noise|occlusion|clutter|reacquisition` - (`docs/MVP_GOLDEN_DEMO.md`) — each a self-contained, reproducible condition tied to a - specific measured finding above. -- **Full latency budget measured** (`docs/MVP_METRICS.md §5`, `mvp_latency_budget`): - every configuration, including Hybrid+Tracker, fits inside the 20 ms / 50 Hz budget - at P95 on this development machine (not a hardware claim). -- The 27-column Step-8 telemetry CSV now carries 42 columns total (7 Stage-3 perception - + 8 P0-v2 tracker fields, both additive and header-driven — old readers unaffected); - Mission Control's telemetry rail gained a live "STATE ESTIMATOR" panel. - -## macOS quick start + distractor defeats this mitigation completely (`docs/MVP_ABLATION.md §6`) — the gate + rejects spatially/temporally *incoherent* candidates, not any adversarial one. +- **Full latency budget measured**: every configuration, including Hybrid+Tracker, fits + inside the 20 ms / 50 Hz budget (~1.2 ms P95) on this development machine — not a + hardware claim. +- Telemetry: the CSV export carries 42 columns total (27 core + 7 AI-perception + 8 + tracker, all additive and header-driven — old readers unaffected). + +## Mission Control (frontend) + +A Next.js UI that visualizes the real telemetry above — it never fakes data, which is +enforced by an automated Playwright guard that fails the build if any application +source contains `Math.random`. + +```bash +cd frontend && npm install +npm run dev # http://localhost:4317 +``` + +- **ENGINE mode** runs the actual `fsoc_demo` C++ binary live and streams its real CSV + output over `/api/simulation/:scenario`. +- **REPLAY mode** plays back a checked-in, deterministic recording of that same binary + (useful when the C++ build isn't available locally). + +Neither mode is a physical test bench — see **Hardware boundary**. The top bar labels +the session **"Simulation"** explicitly and reads "Sim Feed Active/Fault" (not +"Uplink") so it can never be misread as a live hardware/RF connection. + +## Quick start ```bash +# --- one-time setup (macOS) --- xcode-select --install # only if Command Line Tools are missing -brew install cmake ninja -brew install opencv # required from Step 4 onward +brew install cmake ninja opencv +# --- build + test the C++ engine --- cmake --preset debug cmake --build --preset debug -ctest --preset debug -./build/debug/step1_math_smoke -./build/debug/step2_trajectory_smoke -./build/debug/step3_observation_smoke -./build/debug/step4_renderer_smoke -./build/debug/step5_detector_smoke -./build/debug/step6_pid_smoke -./build/debug/step7_closed_loop_smoke -./build/debug/step8_telemetry_smoke # writes generated/step8_*.csv -./build/debug/step9_visualization_smoke # writes generated/step9/*.png (+ optional .mp4) -./build/debug/step10_validation_smoke # baseline acceptance; writes generated/step10/ -./build/debug/fsoc_demo sinusoidal # demo runner: static|sinusoidal|loss|open|closed -./build/debug/fsoc_demo static --mode hybrid # same demo, live AI + Safe Hybrid perception -./build/debug/fsoc_demo static --mode hybrid --tracker # + P0-v2 state estimator (Hybrid V2) -./build/debug/fsoc_demo clutter # named disturbance preset: normal|noise|occlusion|clutter|reacquisition -make demo # reproducible: validation + demos + visualization - -# AI perception (requires the committed models/tiny_beacon_net.onnx, already in the repo) -./build/debug/ai_inference_benchmark # C++ ONNX inference latency, this machine -cmake --preset release && cmake --build --preset release -./build/release/stage4_evaluation --out generated/ai_stage4 # full frozen-protocol eval, ~10-15 min -./build/release/stage4_tracker_ablation --out generated/ai_stage4_ablation # P0-v2 clutter mitigation, ~15 min -./build/release/mvp_dynamic_scenarios --out generated/mvp_dynamic_scenarios # velocity/dropout/moving-clutter scenarios -./build/release/mvp_latency_budget # full latency budget, seconds +ctest --preset debug # 17/17 suites + +# --- run the demo --- +./build/debug/fsoc_demo normal # base scenarios: static|sinusoidal|loss|open|closed +./build/debug/fsoc_demo static --mode hybrid --tracker # AI + Safe Hybrid + state estimator +./build/debug/fsoc_demo clutter # named disturbance preset (see "Working demo") -# Mission Control frontend (Next.js; reads real fsoc_demo CSV output, never fakes telemetry) +# --- Mission Control frontend --- cd frontend && npm install -npm run dev # http://localhost:4317 — toggle ENGINE (live fsoc_demo) / REPLAY (checked-in fixture) -npm run typecheck && npm run lint && npm run build -npx playwright test # end-to-end smoke suite, incl. a no-Math.random anti-fake-data guard +npm run dev # http://localhost:4317 ``` -Steps 1–3 build and pass without OpenCV; if `opencv` is missing, CMake prints a notice -and skips the Step 4 renderer target only. Install it with `brew install opencv` and -reconfigure — no Homebrew paths are hardcoded. - -## Simulation vs. real hardware, and current limitations - -Everything in this repository — every metric, every demo, every frontend view — runs -against the deterministic C++ **simulation** (`SyntheticCameraRenderer` draws an analytic -Gaussian beacon; there is no physical camera, beacon, or pan/tilt mechanism anywhere in -this codebase). Mission Control's `ENGINE`/`REPLAY` toggle distinguishes "the real -simulation binary, run live" from "a checked-in deterministic recording of that same -binary" — neither is a physical test bench. See `docs/MVP_METRICS.md` §5 for the full -"not measured / not claimed" list. - -The architecture is deliberately layered so `FrameSource` / `Detector` / `Controller` / -`PanTiltCamera` are independently swappable (`docs/09_FUTURE_ARCHITECTURE.md`): the next -hardware step is replacing `SyntheticCameraRenderer` with a real frame grabber and -`PanTiltCamera::step()`'s actuator model with a real servo/motor driver, without touching -the detector, PID, or `SimulationRunner` step order. Known MVP-stage limitations: AI -recall is intentionally low (~16-40% depending on scenario) rather than over-confident; -a minimal alpha-beta state estimator now exists and measurably mitigates (does not -solve) Classical's clutter false-lock behavior for spatially/temporally *incoherent* -candidates — a *temporally coherent* (smoothly moving) distractor still defeats it -completely, a real, disclosed, currently-unresolved gap (`docs/MVP_ABLATION.md`); the -estimator is a P-dominant plant + alpha-beta filter (kp=12, ki=0, kd=0), not a Kalman/UKF -— a deliberate, documented, currently-sufficient choice -(`docs/16_BASELINE_ACCEPTANCE.md`, `include/fsoc/target_tracker.hpp`), not an oversight. - -## Repository layout +Steps that need OpenCV are auto-skipped with a one-line CMake notice if it isn't +installed — no Homebrew paths are hardcoded. Ubuntu/CI equivalents: +`sudo apt-get install -y ninja-build libopencv-dev` (see `.github/workflows/ci.yml`, +which runs this exact pipeline on every push/PR). + +## Validation + +Three independent layers, all reproducible locally: + +```bash +ctest --preset debug # 17/17 C++ suites +./build/debug/step10_validation_smoke # 7/7 baseline gates +cmake --preset release && cmake --build --preset release +./build/release/stage4_evaluation --out generated/ai_stage4 # ~10-15 min, frozen protocol +./build/release/stage4_tracker_ablation --out generated/ai_stage4_ablation # ~15 min, clutter mitigation +./build/release/mvp_dynamic_scenarios --out generated/mvp_dynamic_scenarios # velocity/dropout/moving-clutter +./build/release/mvp_latency_budget # full latency budget, seconds +cd frontend && npm run typecheck && npm run lint && npm run build && npx playwright test +``` + +`docs/16_BASELINE_ACCEPTANCE.md`, `docs/21_AI_STAGE4_EVALUATION_PROTOCOL.md`, +`docs/MVP_ABLATION.md`, and `docs/SIH_MVP_FREEZE.md` are the canonical, frozen sources +for what each gate/protocol measures and why. + +## Known limitations + +State these proactively — they're disclosed in `docs/SIH_MVP_FREEZE.md`, not buried: + +1. **A temporally coherent moving distractor defeats the clutter mitigation + completely** — identical outlier counts with or without the tracker + (`docs/MVP_ABLATION.md §6`). The single most important limitation. +2. **The intrinsic single-frame classical false-positive rate (44.9%) is unchanged** — + unfixable without touching the frozen classical detector algorithm. +3. **AI-only reacquisition through `resolve_perception()` is not implemented** — the + prerequisite gate exists (ADR-019); unlocking it is a distinct, deliberately + deferred change to the frozen fusion policy. +4. AI recall is intentionally low (~16-40% depending on scenario) rather than + over-confident — a deliberate precision-over-recall design choice, not a bug. +5. Real coverage cost: Hybrid+Tracker trades ~20 points of coverage for the outlier + reduction above — a disclosed trade, not a free win. +6. Evaluated at n=5 seeds per scenario (matches the frozen protocol) — real, not + large-sample, statistics. + +## Hardware boundary + +Zero physical camera, beacon, servo, or pan/tilt hardware has been used anywhere in +this project. Every number in this README comes from the deterministic C++ simulation +on a desktop-class development machine (Apple M5). **No claim of embedded, flight, or +real-time-on-target hardware performance is made or implied anywhere in this +repository.** See `docs/MVP_METRICS.md §5` and `docs/SIH_MVP_FREEZE.md §6`. + +## Repository structure ```text include/fsoc/ Public interfaces @@ -385,11 +273,20 @@ models/ Committed trained ONNX model + metadata (models/MODEL_CARD.m tools/ai/ Offline Python training toolchain (NOT part of the C++ runtime) frontend/ Next.js Mission Control UI (reads real fsoc_demo telemetry) cmake/ Build policies +.github/workflows/ CI (build + test, C++ and frontend) .claude/skills/ Claude Code engineering skills .claude/agents/ Specialist subagents -docs/ PRD/SRS/design/roadmap/test plans/AI architecture/measured metrics -prompts/ Reusable Vibe Coding prompts -generated/ Git-ignored run artifacts (CSV/PNG/JSON reports) — never committed +docs/ Design docs, measured metrics, ADRs, development history +prompts/ Reusable Vibe Coding prompts +generated/ Git-ignored run artifacts (CSV/PNG/JSON reports) — never committed ``` -Read `CLAUDE.md` before asking an AI coding agent to modify the project. +## Engineering history & deeper docs + +- **Full documentation index**: `docs/README.md` +- **Chronological build log** (Step 1 → Step 11, exact numbers at each stage): + `docs/DEVELOPMENT_HISTORY.md` +- **Every architecture/algorithm decision and its evidence** (20 ADRs): `DECISIONS.md` +- **Frozen SIH MVP release state**: `docs/SIH_MVP_FREEZE.md` + +Read `CLAUDE.md` before asking an AI coding agent to modify this project. diff --git a/docs/DEVELOPMENT_HISTORY.md b/docs/DEVELOPMENT_HISTORY.md new file mode 100644 index 0000000..6c8a63f --- /dev/null +++ b/docs/DEVELOPMENT_HISTORY.md @@ -0,0 +1,259 @@ +# Development History — Step 1 through Step 11 + +The chronological build log of the classical `v1_baseline` engine, moved here from +`README.md` to keep the top-level landing page judge-facing. Nothing below is +retuned or reinterpreted — it is the original record of what was implemented, in +order, with the exact test counts and numbers measured at each stage. For the +post-`v1_baseline` AI perception (Stage 2-4) and state-estimation (P0-v2) work, see +`README.md`'s own sections (they're recent enough to stay on the landing page) plus +`docs/MVP_METRICS.md` / `docs/MVP_ABLATION.md` for full measured detail. + +## Language decision + +The project baseline is **modern C++20**. There is no Python package, virtual +environment, pip install, `pyproject.toml`, NumPy, or PyVista dependency in the core +project — i.e. in the **runtime control loop**. `tools/ai/` is a separate, offline, +one-time model-training toolchain (PyTorch → ONNX export, `tools/ai/README.md`) that +produces the committed `models/tiny_beacon_net.onnx`; it is never imported, run, or +required by the C++ runtime, which loads that ONNX file through OpenCV-DNN. + +For the first 48-hour MVP: +- Core math/physics/control: C++20 +- Build: CMake + Ninja +- Pixel simulation/tracking visualization: OpenCV C++ (introduced after the math gate) +- Step-1 vector math: dependency-free to keep the foundation auditable +- Later UKF/MPC phase: add Eigen when matrix-heavy estimation/control begins + +## Step 1 already implemented + +- 3D world convention +- pan/tilt camera basis +- pinhole projection +- finite camera FOV +- actuator velocity saturation +- tilt mechanical limits +- ideal pointing angles for diagnostics only +- terminal-only smoke test +- 12 unit checks using CTest, with no external test framework + +## Step 2 implemented — target trajectory engine + +- `TargetState` = world `position_m` + `velocity_mps` (SI, double precision) +- `Trajectory` abstract interface: pure `state_at(double time_s)`, no owned clock +- stationary, linear constant-velocity (signed), and sinusoidal trajectories +- sinusoidal velocity is the exact analytic derivative; frequency in Hz (`omega = 2*pi*f`) +- deliberate input validation via `std::invalid_argument` (non-finite / negative time, + non-finite params, negative frequency/amplitude) +- `step2_trajectory_smoke` + deterministic analytic CTest suite (`fsoc_step2_tests`) +- no coupling to camera / perception / control + +## Step 3 implemented — observation / measurement / tracking-error contracts + +- strongly typed layers kept distinct: `TargetState` (truth) → `CameraObservation` / + `Projection` (exact projection) → `BeaconDetection` (image estimate) → `TrackingError` + (controller-facing) +- `ObservationStatus` = `Visible` / `OutsideFieldOfView` / `BehindCamera`; + `observe_beacon()` reuses `PanTiltCamera::project()` — no duplicated projection math +- frozen image convention: origin top-left, `+x_px` right, `+y_px` down; + centre `cx = W/2.0`, `cy = H/2.0` owned by the camera +- frozen sign convention: pixel `error_x>0` = RIGHT, `error_y>0` = BELOW; + angular `pan_rad>0` = command pan right, `tilt_rad>0` = command tilt up +- `compute_tracking_error(std::optional, PanTiltCamera)` — `optional` + in / out, non-finite centroid rejected, reuses `pixel_error_to_angles` +- target-lost = empty `std::optional` only (no `(-1,-1)` / NaN / zero sentinels) +- `step3_observation_smoke` (machine-checks the critical (400,180) scenario) + + `fsoc_step3_tests` (all four quadrants, pinhole match, regressions) +- no OpenCV, no detector algorithm, no controller + +## Step 4 implemented — synthetic virtual-camera image renderer + +- first OpenCV use, isolated in a separate `fsoc_render` library; `fsoc_core` and all + pure-math headers stay OpenCV-free +- `SyntheticCameraRenderer::render(const CameraObservation&) -> cv::Mat` (`CV_8UC1`) +- uniform dark background (default 5 counts) + analytic 2-D Gaussian beacon + (peak 255, `sigma` in **pixels**), clamped to `[0,255]` +- **true sub-pixel** beacon centre — the fractional `ImagePoint` is never rounded before + the Gaussian is evaluated, so a weighted centroid recovers it +- edge-safe: the Gaussian is rasterised in a window clipped to the image +- `OutsideFieldOfView` / `BehindCamera` → background-only frame (no fake beacon) +- no sensor noise this step; the same observation renders byte-identical frames +- `step4_renderer_smoke` writes `generated/*.png` headlessly + `fsoc_step4_tests` +- CMake `find_package(OpenCV)` auto-detected (`FSOC_ENABLE_OPENCV=AUTO|ON|OFF`) + +## Step 5 implemented — baseline beacon detector + +- new `fsoc_perception` library (`fsoc::core` + OpenCV core/imgproc); **does not depend on + `fsoc_render`** — `BeaconDetector::detect(const cv::Mat&)` consumes pixels only, never + `TargetState` / trajectory / `CameraObservation` / the projected `ImagePoint` +- transparent pipeline: threshold (`pixel >= threshold_intensity`, default 64) → + 8-connected components → reject `area < min_bright_pixels` → pick the component with the + greatest integrated signal (ties: lowest label) → intensity-weighted centroid +- centroid weight `= (pixel − threshold) + 1` (no assumed background); recovers the + Gaussian's sub-pixel centre to **≈ 0.02 px** on clean interior frames (gate 0.15) +- found → `std::optional` (Step-3 type); not found → `std::nullopt` + (no `(-1,-1)` / NaN / zero sentinel); no fabricated confidence +- rejects empty / non-`CV_8UC1` frames with `std::invalid_argument` +- perception chain verified: renderer → detector → `compute_tracking_error` reproduces + RIGHT+ABOVE → pan > 0, tilt > 0 +- `step5_detector_smoke` (headless, `std::chrono` timing for curiosity) + `fsoc_step5_tests` + +## Step 6 implemented — pan/tilt PID controller + +- new `fsoc_control` library depending on **`fsoc::core` only** (via `fsoc/tracking_error.hpp`); + **OpenCV-free** — links no OpenCV / `fsoc_render` / `fsoc_perception`, builds without OpenCV +- `PIDController::update(const TrackingError&, double dt_s) -> ControlCommand` — angular + error (radians) in, pan/tilt **rate** (rad/s) out; never absolute angles, never touches + `PanTiltCamera` +- two independent axes, standard discrete PID `u = kp·e + ki·I + kd·D` on `angular.pan_rad` + / `angular.tilt_rad`; derivative forced to 0 on the first update after construction/`reset()` +- anti-windup: integral hard-clamped to ±`integral_limit` + conditional integration; output + clamped to ±`output_limit_rad_s` +- `reset()` clears integrals / previous errors / first-sample flags; `zero_control_command()` + helper for the runner's target-loss path +- rejects non-finite / ≤0 `dt_s` and non-finite `TrackingError` with `std::invalid_argument` + (state untouched on throw); invalid config rejected at construction +- sign preserved: `e > 0` (RIGHT / ABOVE) → command > 0 (PAN RIGHT / TILT UP) +- default gains are **untuned placeholders** (tuned in Step 7) +- `step6_pid_smoke` (5 scenarios + toy scalar-plant sanity) + `fsoc_step6_tests` + +## Step 7 implemented — closed-loop tracking simulation + +- new `fsoc_simulation` library (links `fsoc::core` + `render` + `perception` + `control`) + — the **one** intentional integration layer; owns the clock, fixed timestep, subsystem + call order, target-loss policy, and camera stepping, and **no** domain math +- `SimulationRunner::step()` runs one fixed timestep in this order: `trajectory.state_at(t)` + → `observe_beacon` → `renderer.render` → `detector.detect(cv::Mat)` → + `compute_tracking_error(detection, camera)` → `pid.update` (or loss policy) → + `camera.step` → record `SimulationStepResult` → `t += dt` +- fixed `dt = 0.02 s` (50 Hz); **never wall-clock**; same config + trajectory → + bit-identical result sequence +- **pixel-only feedback:** control is driven solely by the detected centroid; + `TargetState` / `observation.image_point_px` / exact `Projection` feed only the labelled + diagnostic fields and truth-vs-measurement scoring — proven by + `test_control_follows_detected_not_truth` +- **target-loss policy:** no detection → `pid.reset()` + zero command + camera holds (no + search); the loop resumes from reset if the target drifts back into the FOV +- `SimulationRunnerConfig::validate()` rejects PID output limit > camera actuator rate and + renderer/camera dimension mismatch +- empirically-tuned MVP baseline PID **kp = 12, ki = 0, kd = 0** (P-dominant on the + integrator plant — not claimed optimal); results: static acquisition **4.13° → 0.0° in + ~0.34 s**, sinusoidal (±12.4°) RMS **0.55°** at 100 % detection, open-loop → closed-loop + detection **57 % → 100 %** and RMS **6.45° → 0.55°** +- `step7_closed_loop_smoke` (static / sinusoidal / open-vs-closed) + `fsoc_step7_tests` + +## Step 8 implemented — telemetry + benchmarking + +- new `fsoc_telemetry` library — an **observer**: consumes `SimulationStepResult`, never + calls back into the loop. Running with vs without telemetry yields a bit-identical + `SimulationStepResult` sequence (mandatory non-interference test) +- `TelemetryRecord` — 27 flat, unit-suffixed, JSON-mappable fields; unavailable + measurements are `std::optional` in memory (**no `-1` / NaN / `N/A` sentinel**) and empty + fields in CSV; `TrackingState { Tracking, TargetLost }` +- `CsvTelemetryLogger` — synchronous `std::ofstream`, one flushed line per record, no + threads / async / external CSV dependency; writes `generated/step8_*.csv` (git-ignored) +- `BenchmarkMetrics` / `compute_benchmark_metrics` — detection %, RMS/mean/max/final/**P95** + angular error, mean/RMS/max pixel error, mean detection error, command/pan/tilt + saturation fractions, mean|abs|+peak applied rates, wall time + processing FPS. Error + metrics over frames with a `TrackingError`; percentile = nearest-rank `ceil(0.95·N)-1` +- **wall clock vs simulation clock:** physics stays on the fixed `dt = 0.02 s` (50 Hz); + `processing_fps = frames / wall_time` is measured with `std::chrono` around the step loop + only (~4700 FPS ≈ 90× real time) and never feeds the sim dt +- `step8_telemetry_smoke` runs the 4 benchmark scenarios, exports CSVs, prints the + comparison table (Static P95 0.17°, Sinusoidal-closed P95 0.79° vs Sinusoidal-open P95 + 9.81°) + `fsoc_step8_tests` + + (`TelemetryRecord` has since grown additively to 42 columns — 7 Stage-3 AI-perception + fields and 8 P0-v2 tracker fields appended after this step; see `docs/08_TELEMETRY_SCHEMA.md`.) + +## Step 9 implemented — engineering camera-view visualization + +- new `fsoc_visualization` library — an **observer**: `TrackingVisualizer::annotate()` takes + the perception `CV_8UC1` frame **by const& (never modified)** and returns a **new + `CV_8UC3` BGR** display frame. The control path keeps running on the original unannotated + image; overlay pixels can never reach the detector +- `SimulationRunner` / `SimulationStepResult` **not changed** — the base frame is + reconstructed from `result.observation` via a deterministic `SyntheticCameraRenderer` +- overlays: centre crosshair (from frame geometry, not hardcoded 320/240), detection marker + at `telemetry.detected_*`, centre→detected error vector (shrinks to zero on convergence), + `TRACKING` / `TARGET LOST`, `VISIBLE` vs `DETECTED`, SIM/FRAME, PAN/TILT (deg), ANG ERR + (deg), ERR PX, CMD rates (deg/s) with amber `RATE LIMIT` from the Step-8 `*_saturated` + flags +- colours: green = tracking, red = lost, amber = saturation, grey = neutral; optional + `DETECT ERR` and `TRUTH` square marker are **off by default** +- headless: PNG per selected frame (required) + optional best-effort `cv::VideoWriter` MP4 + (graceful `false` when no codec / no `videoio`); output → `generated/step9/` (git-ignored) +- mandatory non-interference test passed (500 frames with/without annotation → identical + `SimulationStepResult` sequence); static story visually verified (frame 0: 4.13° / long + vector / 30°/s + `RATE LIMIT` → final: beacon on crosshair / 0.00° / 0°/s) +- `step9_visualization_smoke` (static / sinusoidal / target-lost) + `fsoc_step9_tests` + +## Step 10 implemented — baseline acceptance / validation suite + +- new `fsoc_validation` library (links `fsoc::simulation` + `fsoc::telemetry` + + `fsoc::visualization`) — an **evaluation layer**. It runs the *existing* v1 system across + seven named deterministic scenarios and checks acceptance gates; it implements **no** + trajectory / detector / PID / renderer / camera math and never controls the loop or + changes an algorithm to improve a number +- **gates are frozen up front** in `docs/16_BASELINE_ACCEPTANCE.md` — physically justified, + documented, **not** derived from the run being scored. Baseline PID stays **kp = 12, + ki = 0, kd = 0** +- scenarios: **A** Static Acquisition · **B** Slow Linear Tracking · **C** Sinusoidal + Tracking · **D** Near-FOV-Edge Acquisition · **E** Actuator Saturation · **F** Target + Loss and Re-entry · **G** Open Loop vs Closed Loop +- per-scenario global checks: finite values (no NaN/Inf), monotonic timestamps, fixed dt, + command rate ≤ PID limit, applied rate ≤ actuator limit, target-loss semantics, and a + **deterministic-replay** check (a second independent run is bit-identical) +- a **mandatory failure-check test** injects an impossible gate and tightens a real + threshold past its actual value and confirms the evaluator then reports FAIL — it is not + an always-green harness +- `step10_validation_smoke` prints a judge-friendly table, writes CSV + annotated PNG + evidence and `generated/step10/VALIDATION_REPORT.md` (values generated from the run, not + hardcoded), and ends with `STEP 10 BASELINE ACCEPTANCE: PASS` — **7 / 7 scenarios pass** + (static 4.13° → 0.00°; sinusoidal RMS 0.55°; open→closed detection 57.4 % → 100 %, RMS + 6.45° → 0.55°, ×11.8). `fsoc_step10_tests` green (10 checks) +- generated evidence → `generated/step10/` (git-ignored); the canonical gate definitions + live in `docs/16_BASELINE_ACCEPTANCE.md` (committed). The **`v1_baseline` tag is created + and pushed** to `origin`, pointing at the merged Step‑10 baseline (`20c028c`); that + validated baseline is **frozen** + +## Step 11 implemented — demo freeze + frontend data contract prep + +- new `fsoc_demo_support` library (links `fsoc::simulation` + `fsoc::telemetry`) — an + **additive** presentation layer built *after* the frozen baseline. `v1_baseline` (tag, + pushed to `origin`) stays put; this step changes **no** validated algorithm. Geometry / + camera / trajectory / renderer / detector / `TrackingError` / PID law / PID gains / + `SimulationRunner` order / Step-10 gates are all untouched. +- **`DemoScenario`** — `static` · `sinusoidal` · `loss` · `open` · `closed`, selected by a + clean token (`parse_demo_scenario`). Each reuses the validated Step-10 trajectory/config + **verbatim** (no retuning). `open` and `closed` share identical trajectory parameters — + only `control_enabled` differs. +- **`DemoSnapshot`** — a per-frame view model for a future UI, built **only** from + `SimulationStepResult` + `TelemetryRecord` + `CameraConfig` by `make_demo_snapshot()`. It + never participates in control. Optionals are `std::nullopt` when the target is lost — no + sentinels. Fields + the future JSON shape are frozen in + `docs/18_FRONTEND_DATA_CONTRACT.md`. +- **Units** — core stays radians / rad·s⁻¹ / m / m·s⁻¹ / px. Degrees appear only via + `to_degrees(const DemoSnapshot&)` at the UI boundary; core physics units are unchanged. +- **`DemoSession`** — deterministic packaging of one scenario: owns a heap `Trajectory` + (constructed before, so it outlives the `SimulationRunner`) + the runner + the telemetry + conversion. `DemoRunState { Ready, Running, Paused, Finished }` is application state, + distinct from `TrackingState`; a **paused `step()` does not advance simulation time**. + `reset()` reproduces a bit-identical run. +- **`fsoc_demo` CLI** — `./build/debug/fsoc_demo [--duration s] [--csv path] + [--quiet]` / `--help`. Per-frame status lines + an end-of-run summary (detection %, RMS / + P95 / max angular error, lost frames) computed by the existing Step-8 `BenchmarkMetrics`. +- **non-interference** (mandatory) — for all 5 scenarios a bare `SimulationRunner` and the + `DemoSession` produce field-identical `SimulationStepResult` sequences. `fsoc_step11_tests` + green (23 checks at the time of this step; since grown to 32 with the P0-v2 additions). + Demo numbers match Step 10 exactly. +- reproducible: `make demo` (or `scripts/run_baseline_demo.sh`) runs the Step-10 validation + + the static & sinusoidal demos + the Step-9 visualization evidence. See + `docs/17_DEMO_FREEZE.md` for the teammate-Mac checklist. + +## What came after Step 11 + +Stage 2-4 (AI perception: TinyBeaconNet training, C++ ONNX inference, Safe Hybrid fusion, +frozen Stage-4 evaluation) and P0-v2 (alpha-beta state estimation, clutter mitigation, +demo disturbance presets, latency budget) are recent enough to stay on the main +`README.md` landing page rather than move here. Full measured detail for both: +`docs/MVP_METRICS.md`, `docs/MVP_ABLATION.md`, `DECISIONS.md` (ADR-015 through ADR-019). diff --git a/FILES.txt b/docs/archive/FILES.txt similarity index 94% rename from FILES.txt rename to docs/archive/FILES.txt index f19d124..58f36d2 100644 --- a/FILES.txt +++ b/docs/archive/FILES.txt @@ -1,3 +1,7 @@ +# ARCHIVED: a stale, manually-maintained file listing from early in the project. Does +# not reflect the current repository tree (hundreds of files added since). Kept for +# history only — use `git ls-files` for a current listing. + .clang-format .clang-tidy .claude/agents/gnc-geometry-engineer.md diff --git a/KIT_MANIFEST.md b/docs/archive/KIT_MANIFEST.md similarity index 67% rename from KIT_MANIFEST.md rename to docs/archive/KIT_MANIFEST.md index 9fd8f9f..ea0a122 100644 --- a/KIT_MANIFEST.md +++ b/docs/archive/KIT_MANIFEST.md @@ -1,3 +1,8 @@ +> **Archived.** Written at the very start of the project (Python→C++ starter-kit +> conversion); the "63 files" count and asset list are historical, not current. Kept for +> engineering-history continuity, not as a live reference — see `docs/README.md` for the +> current documentation index. + # Kit Manifest This is the C++20 replacement for the original Python FSOC Vibe Coding starter. diff --git a/MIGRATION_FROM_PYTHON.md b/docs/archive/MIGRATION_FROM_PYTHON.md similarity index 84% rename from MIGRATION_FROM_PYTHON.md rename to docs/archive/MIGRATION_FROM_PYTHON.md index 7fd84b3..06c3b4c 100644 --- a/MIGRATION_FROM_PYTHON.md +++ b/docs/archive/MIGRATION_FROM_PYTHON.md @@ -1,3 +1,6 @@ +> **Archived.** A one-time bookkeeping note from the original Python→C++20 conversion at +> the very start of the project. Purely historical. + # Migration From Python Starter The project has been converted to C++20. diff --git a/VALIDATION.txt b/docs/archive/VALIDATION.txt similarity index 98% rename from VALIDATION.txt rename to docs/archive/VALIDATION.txt index 3a365d3..cd2f49b 100644 --- a/VALIDATION.txt +++ b/docs/archive/VALIDATION.txt @@ -1,3 +1,7 @@ +# ARCHIVED: a manually-appended, Step-1-era validation log. Fully superseded by +# docs/16_BASELINE_ACCEPTANCE.md, docs/MVP_METRICS.md, and docs/SIH_MVP_FREEZE.md, +# which are the current, canonical sources for measured results. Kept for history only. + SIH26169 FSOC C++ starter validation Validated in packaging environment with CMake 3.31 / C++20: From 6d9bddc387e3c5a5186964ac8451b44adca45241 Mon Sep 17 00:00:00 2001 From: Kirtan Date: Mon, 7 Sep 2026 09:50:31 +0530 Subject: [PATCH 2/5] ci: add automated C++ and frontend validation Adds .github/workflows/ci.yml -- previously only a .github/workflows_placeholder.md existed, no automated build/test signal on this public repository at all. Two jobs, both on ubuntu-latest, on every pull_request and push to main: - cpp: installs CMake/Ninja via pip (avoids relying on the runner's preinstalled cmake version, which can lag the project's cmake_minimum_required 3.24) and libopencv-dev via apt (verified against actual usage in the codebase -- no OpenCV-5-specific API is used, only stable 4.x-compatible calls), then runs the exact local pipeline: cmake --preset debug, cmake --build --preset debug, ctest --preset debug. - frontend: npm ci, typecheck, lint, production build, then Playwright E2E against the REPLAY fixtures. playwright.config.ts uses channel:"chrome" (the system browser, not a Playwright-managed download), so Chrome is installed explicitly via browser-actions/setup-chrome rather than assumed present on the runner image. Validated locally: `ruby -ryaml` confirms valid YAML syntax; `actionlint` (installed via Homebrew for this pass) reports zero issues against the workflow file. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014h1TXJD8THZH4NbMqzrx97 --- .github/workflows/ci.yml | 76 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..26c737c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +name: Build & Test + +on: + pull_request: + push: + branches: [main] + +# Cancel superseded runs on the same ref (saves CI minutes on rapid pushes). +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + cpp: + name: C++ (build + ctest) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Ubuntu's apt cmake can lag the project's cmake_minimum_required (3.24); + # the pip wheel is always current and avoids guessing at the runner's + # preinstalled version. + - name: Install CMake + Ninja + run: python3 -m pip install --upgrade cmake ninja + + - name: Install OpenCV (core, imgproc, imgcodecs, dnn) + run: | + sudo apt-get update + sudo apt-get install -y libopencv-dev + + - name: Configure (debug) + run: cmake --preset debug + + - name: Build (debug) + run: cmake --build --preset debug + + - name: Test + run: ctest --preset debug --output-on-failure + + frontend: + name: Frontend (typecheck, lint, build, E2E) + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Production build + run: npm run build + + # playwright.config.ts uses channel:"chrome" (the system browser, not a + # Playwright-managed download) — install it explicitly rather than + # assuming it's preinstalled on the runner image. + - name: Install Google Chrome + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable + + - name: End-to-end tests (Playwright, REPLAY fixtures) + run: npx playwright test From fd5f6d9ecc0f33cf271944334bb18aa848b72d84 Mon Sep 17 00:00:00 2001 From: Kirtan Date: Mon, 7 Sep 2026 09:50:45 +0530 Subject: [PATCH 3/5] docs: add repository documentation index Adds docs/README.md as a reviewer's map through the docs/ directory, grouped the way a reviewer actually needs it (Start Here / Architecture / Validation / Design Decisions / Development History / Everything Else) rather than the existing numeric 00-21 ordering, which reflects build chronology, not reading priority. Points to, rather than duplicates, every existing doc -- no content is copied or re-summarized. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014h1TXJD8THZH4NbMqzrx97 --- docs/README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9e442b1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,57 @@ +# Documentation Index + +A reviewer's map through this repository's docs. Start at the top and go as deep as you +need — nothing here duplicates another file; each doc is the single canonical source +for what it covers. + +## Start here + +| doc | what it's for | +|---|---| +| [`SIH_MVP_FREEZE.md`](SIH_MVP_FREEZE.md) | The frozen SIH MVP state: build/test status, measured metrics, safe vs. unsafe claims for a judge presentation, exact demo commands | +| [`MVP_GOLDEN_DEMO.md`](MVP_GOLDEN_DEMO.md) | The 15-step, fully reproducible judge-session walkthrough | +| [`RELEASE_NOTES_v2_sih_mvp.md`](RELEASE_NOTES_v2_sih_mvp.md) | Draft GitHub release notes for the `v2_sih_mvp` tag (not yet published) | + +## Architecture + +| doc | what it's for | +|---|---| +| [`19_AI_PERCEPTION_ARCHITECTURE.md`](19_AI_PERCEPTION_ARCHITECTURE.md) | Classical + AI + Safe Hybrid fusion design (ADR-015 through ADR-018) | +| [`09_FUTURE_ARCHITECTURE.md`](09_FUTURE_ARCHITECTURE.md) | The swappable-interface boundary for a future real-hardware port | +| [`15_INTERFACE_CONTRACTS.md`](15_INTERFACE_CONTRACTS.md) | Frozen module boundaries and data contracts | +| [`18_FRONTEND_DATA_CONTRACT.md`](18_FRONTEND_DATA_CONTRACT.md) | The C++ → frontend `DemoSnapshot` transport shape | +| [`08_TELEMETRY_SCHEMA.md`](08_TELEMETRY_SCHEMA.md) | The 42-column CSV telemetry schema, field by field | +| [`04_COORDINATES_AND_MATH.md`](04_COORDINATES_AND_MATH.md) | The frozen world/camera/image coordinate conventions | + +## Validation + +| doc | what it's for | +|---|---| +| [`MVP_METRICS.md`](MVP_METRICS.md) | Consolidated real measured numbers across every stage, including the full latency budget | +| [`MVP_ABLATION.md`](MVP_ABLATION.md) | The clutter false-lock investigation, the state-estimator mitigation, and the A/B/C ablation, all measured | +| [`21_AI_STAGE4_EVALUATION_PROTOCOL.md`](21_AI_STAGE4_EVALUATION_PROTOCOL.md) | The frozen Classical/AI/Hybrid evaluation protocol | +| [`16_BASELINE_ACCEPTANCE.md`](16_BASELINE_ACCEPTANCE.md) | The 7 frozen Step-10 baseline acceptance gates | +| [`07_TEST_AND_VALIDATION_PLAN.md`](07_TEST_AND_VALIDATION_PLAN.md) | The original test/validation strategy | + +## Design decisions + +| doc | what it's for | +|---|---| +| [`../DECISIONS.md`](../DECISIONS.md) | Every architecture decision (ADR-001 through ADR-019) with the evidence behind it | + +## Development history + +| doc | what it's for | +|---|---| +| [`DEVELOPMENT_HISTORY.md`](DEVELOPMENT_HISTORY.md) | The chronological Step 1 → Step 11 build log (moved out of `README.md` to keep the landing page judge-facing) | +| [`archive/`](archive/) | Superseded, purely historical bookkeeping from the original Python→C++ starter-kit conversion | + +## Everything else + +`00_PROJECT_BRIEF.md` / `01_PRD.md` / `02_SRS.md` / `03_TECHNICAL_DESIGN.md` / +`05_48_HOUR_ROADMAP.md` / `06_DEFINITION_OF_DONE.md` / `10_DEMO_AND_JUDGING_STORY.md` / +`11_RISK_REGISTER.md` / `12_EXPERIMENT_PROTOCOL.md` / `13_GIT_WORKFLOW.md` / +`14_TASK_BOARD.md` / `16_AI_CODING_GUARDRAILS.md` / `17_CLAUDE_CODE_USAGE.md` / +`17_DEMO_FREEZE.md` / `20_AI_DATASET_AND_TRAINING.md` / `09_VISUALIZATION.md` — the +original planning/process docs from each build phase. Still accurate for their scope; +not duplicated or re-summarized here. From 9a8250baf15583754aa4f30b531c181888e8b60e Mon Sep 17 00:00:00 2001 From: Kirtan Date: Mon, 7 Sep 2026 09:50:55 +0530 Subject: [PATCH 4/5] docs(release): prepare SIH MVP v2 release notes Drafts GitHub release notes for the v2_sih_mvp tag (already exists, pointing at cc8069e on main -- not moved, not recreated). NOT published: this commit only adds the draft file plus the exact `gh release create` command to publish it once reviewed. Content is restricted to claims already verified in docs/SIH_MVP_FREEZE.md -- project identity, real capabilities, measured metrics, demo commands, known limitations, and the hardware boundary, in that order, with no claim beyond what that document supports. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014h1TXJD8THZH4NbMqzrx97 --- docs/RELEASE_NOTES_v2_sih_mvp.md | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/RELEASE_NOTES_v2_sih_mvp.md diff --git a/docs/RELEASE_NOTES_v2_sih_mvp.md b/docs/RELEASE_NOTES_v2_sih_mvp.md new file mode 100644 index 0000000..f7f2b78 --- /dev/null +++ b/docs/RELEASE_NOTES_v2_sih_mvp.md @@ -0,0 +1,84 @@ + + +## Project identity + +FSOC — autonomous closed-loop coarse alignment for mobile Free-Space Optical +Communication (FSO) terminals. Smart India Hackathon 2026, problem statement SIH26169. +A deterministic C++20 simulation of the full SEE → ESTIMATE → PREDICT → CORRECT loop: +synthetic camera → classical + neural (TinyBeaconNet) perception → Safe Hybrid fusion → +alpha-beta state estimation → PID pan/tilt control. + +## Major capabilities + +- Closed-loop pan/tilt tracking simulation with a validated PID baseline (`v1_baseline`). +- Real TinyBeaconNet CNN (27,282 parameters) trained on a seeded synthetic dataset, + exported to ONNX, and run natively in C++ via OpenCV-DNN — no Python in the runtime path. +- Safe Hybrid fusion policy (ADR-018): classical + AI cross-validation with unconditional + rejection on disagreement — no confidence overrides. +- `TargetTracker`: a minimal alpha-beta state estimator with a temporal-consistency gate + and a bounded coast/reacquire state machine (ADR-019) — deliberately not a Kalman/UKF. +- 5 named, deterministic demo presets (`normal`/`noise`/`occlusion`/`clutter`/`reacquisition`). +- Next.js Mission Control frontend with live (ENGINE) and deterministic-replay (REPLAY) + telemetry modes, and a real-time "STATE ESTIMATOR" diagnostic panel. +- GitHub Actions CI (C++ build+test, frontend typecheck/lint/build/E2E) on every push/PR. + +## Verified metrics + +All measured by committed, deterministic tools in this repository (`docs/MVP_METRICS.md`, +`docs/MVP_ABLATION.md`, `docs/SIH_MVP_FREEZE.md`): + +- Step-10 baseline acceptance: **7/7 PASS** +- C++ test suites: **17/17** (100%) +- Frontend end-to-end tests: **20/20** +- Severe (>50px) closed-loop outliers: Classical 2,240 → Classical+Tracker 12; + Hybrid 1,808 → Hybrid+Tracker 9 (**~99.5% reduction** in both cases) +- Full-step latency, Hybrid+Tracker, P95: **~1.2 ms** against a 20 ms / 50 Hz budget + (development-machine CPU, not a hardware claim) +- Telemetry: 42-column real CSV export + +## Demo commands + +```bash +cmake --preset debug && cmake --build --preset debug && ctest --preset debug +./build/debug/fsoc_demo normal +./build/debug/fsoc_demo clutter +./build/debug/fsoc_demo occlusion +./build/debug/fsoc_demo reacquisition +cd frontend && npm install && npm run dev # http://localhost:4317 +``` + +Full 15-step judge walkthrough: `docs/MVP_GOLDEN_DEMO.md`. + +## Known limitations + +- A temporally coherent (smoothly moving) distractor defeats the clutter mitigation + completely — a disclosed, currently-unresolved gap. +- The intrinsic single-frame classical false-positive rate (44.9%) is unchanged; + unfixable without modifying the frozen classical detector algorithm. +- AI-only reacquisition through the fusion policy is not implemented (deliberately + deferred, ADR-019). +- Real coverage cost: the clutter mitigation trades ~20 points of coverage for the + outlier reduction above. + +## Hardware boundary + +Zero physical camera, beacon, servo, or pan/tilt hardware has been used anywhere in +this project. Every metric above is from the deterministic C++ simulation on a +development machine. No embedded/flight/hardware real-time performance claim is made. + +## Full details + +`docs/SIH_MVP_FREEZE.md` is the canonical source for this release's build/test status, +safe vs. unsafe claims for presentation, and exact reproduction commands. From f6f612d1d17565736bb23dda008af69a47fa6f7f Mon Sep 17 00:00:00 2001 From: Kirtan Date: Mon, 7 Sep 2026 09:58:04 +0530 Subject: [PATCH 5/5] docs(readme): correct demo visual description The Working Demo caption overstated the image's provenance: apps/fsoc_demo.cpp does not use the Step-9 TrackingVisualizer overlay code. Corrects the claim to what's actually true (real Step-9 visualizer output from the deterministic C++ simulation) without overstating a shared code path that doesn't exist. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014h1TXJD8THZH4NbMqzrx97 --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a184ef3..f417c13 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,7 @@ limitations** below for exactly what this does and does not fix. FSOC tracking view — Step-9 visualizer output, static acquisition scenario

-*Real Step-9 visualizer output (the same renderer/overlay code the frontend and CLI use) -— not a mockup.* +*Real Step-9 visualizer output from the deterministic C++ simulation — not a mockup.* Five self-contained, one-command, reproducible conditions (`docs/MVP_GOLDEN_DEMO.md` has the full 15-step judge walkthrough with narration):