Commit Graph

3 Commits

Author SHA1 Message Date
ruv 2d29359809 feat(adr-117/p3+p3.5): vitals + BFLD bindings
P3 — Vital sign extraction bindings (wifi-densepose-vitals):
- VitalStatus enum (eq, eq_int, hash, frozen) — Valid/Degraded/Unreliable/Unavailable
- VitalEstimate (frozen) — value_bpm + confidence + status
- VitalReading (frozen) — HR + BR + signal quality composite
- BreathingExtractor — 0.1–0.5 Hz bandpass + zero-crossing
- HeartRateExtractor — 0.8–2.0 Hz bandpass + autocorrelation
- py.allow_threads on extract() hot loops (Q5 audit confirmed
  core/vitals/signal are pure-sync — zero tokio deps, safe to release
  GIL with no embedded runtime needed)
- 17 tests covering construction, getters, frozen immutability,
  esp32_default + explicit ctors, synthetic-signal end-to-end

P3.5 — BFLD bindings (forward-compat surface, stub Rust):
- BfldKind enum — CompressedHE20/40/80/160 + UncompressedHT20/40
  with n_subcarriers, bandwidth_mhz, is_he metadata getters
- BfldFrame (frozen) — from_compressed_feedback() accepts numpy
  Complex64 ndarray [Nr x Nc x Nsc], validates dims against kind,
  feedback_matrix() returns lossless roundtrip ndarray
- BfldReport — aggregates frames, rejects mismatched kinds,
  computes inverse-CV coherence score
- 19 tests covering all 6 PHY variants + numpy roundtrip +
  dim-mismatch error + aggregation
- Real Rust ingestion (wifi-densepose-bfld crate) lands post-v2.0
  per ADR-117 §11.11/12 — Python API will not change

Total Python test count: 93 (was 57, +36 P3+P3.5). All passing.

Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 11:21:58 -04:00
ruv cbd24cd1ed feat(adr-117/p2): BoundingBox + PersonPose + PoseEstimate — P2 COMPLETE (57/57 tests GREEN)
Lands the second + third chunks of P2: PyO3 bindings for `BoundingBox`,
`PersonPose`, `PoseEstimate` from `wifi_densepose_core`. Combined with
the prior Keypoint + KeypointType bindings (fd0568caa), this closes
ADR-117 §6 P2.

## Coverage

| Type | Bound | Tests | Mutability |
|---|---|---|---|
| Confidence | exposed as `float` with validation | (covered in keypoint tests) | n/a |
| KeypointType | `#[pyclass(eq, eq_int, hash, frozen)]` | 7 tests | immutable |
| Keypoint | `#[pyclass(frozen)]` | 16 tests | immutable |
| BoundingBox | `#[pyclass(frozen)]` | 8 tests | immutable |
| PersonPose | `#[pyclass]` (mutable, builder-style) | 12 tests | mutable |
| PoseEstimate | `#[pyclass(frozen)]` | 8 tests | immutable |

Smoke (P1) + new tests: **57/57 PASS** locally on Windows.

## What's deferred to P3

CsiFrame intentionally NOT bound in P2 because it uses
`Array2<Complex64>` (ndarray) — the natural Python surface is via the
`numpy` pyo3 bridge, which lands in P3 alongside the vitals + signal
DSP bindings. Binding CsiFrame without numpy interop would force
users to materialise lists of tuples which is a worse API than
`csi_frame.amplitude_array()` returning an ndarray.

## Design choices that affect the API surface

1. **PersonPose.keypoints() returns a dict keyed by KeypointType**
   instead of a fixed-length list with None slots. Pythonistas don't
   want to know the underlying storage is `[Option<Keypoint>; 17]`.

2. **PoseEstimate.id and .timestamp exposed as strings** (UUID + ISO)
   rather than as bound `FrameId` / `Timestamp` types. Users in
   notebooks rarely compare UUIDs structurally; strings are good
   enough for diagnostics and don't bloat the bindings.

3. **PersonPose is MUTABLE** (`#[pyclass]` without `frozen`) so users
   can build poses incrementally with `set_keypoint`/`set_bbox`/
   `set_id`. PoseEstimate is `frozen` because once constructed it
   represents a snapshot.

## Three PyO3 0.22 gotchas surfaced this iteration

1. `#[pymethods]` getters are NOT accessible from other Rust modules
   — need a separate `impl PyKeypoint { pub(crate) fn inner(&self)
   -> &Keypoint { ... } }` block for cross-module use.

2. `PyDict::new(py)` was removed in PyO3 0.21 → 0.22 in favour of
   `PyDict::new_bound(py)`. (Confusing because `Bound<'py, PyDict>`
   is the return type either way.)

3. `dict.set_item(K, V)` requires both K and V to impl
   `ToPyObject`. `#[pyclass]` types impl `IntoPy<PyObject>` but NOT
   `ToPyObject` — workaround: convert via `.into_py(py)` first, then
   `set_item(py_object_k, py_object_v)`.

Saved as PyO3 0.22 binding patterns memory at the horizon-tracker
level so future loop workers don't re-learn them.

## Local validation

\`\`\`
$ cd python && .venv/Scripts/python -m pytest tests/ -v
…
======================== 57 passed in 0.24s =========================
\`\`\`

Wheel size: still ~340 KB on Windows release build.

Refs #785, ADR-117 §6 (P2 done — ready for P3 vitals + signal DSP +
numpy bridge + witness v2).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 11:03:02 -04:00
ruv fd0568caa1 feat(adr-117/p2): Keypoint + KeypointType bindings — 23 new tests (29/29 GREEN)
Lands the first chunk of P2: PyO3 bindings for `Keypoint` and
`KeypointType` from `wifi_densepose_core`. Bound types surface to
Python as `wifi_densepose.Keypoint` / `wifi_densepose.KeypointType`.

## Design choices that affect the API surface

1. **`Confidence` is NOT bound as a separate class.** Users hate
   wrapping a float in a constructor. Python-side, confidence is just
   a `float in [0.0, 1.0]`; the binding validates on construction
   (`ValueError` for out-of-range, matching the Rust core error).

2. **`KeypointType` is a `#[pyclass(eq, eq_int, hash, frozen)]` enum**
   — hashable so users can drop it into dicts/sets (the most common
   pattern in pose-analysis notebooks: `keypoints_by_type[k.type] = k`).

3. **`Keypoint.__init__` keyword-only `z`** so 2D users don't have to
   write `None` and 3D users get a clear named arg:
   `Keypoint(KeypointType.LeftWrist, 0.2, 0.4, 0.8, z=0.1)`.

4. **`Keypoint` is `#[pyclass(frozen)]`** — no in-place mutation. The
   Rust core type is immutable through Copy + Hash + Eq, and exposing
   setters from Python would create a copy-vs-reference inconsistency
   between languages.

## Files

- `python/src/bindings/keypoint.rs` — 220 lines of `#[pymethods]`
  wrappers + Rust↔Python enum round-trip
- `python/src/lib.rs` — `mod bindings { pub mod keypoint; }` +
  `bindings::keypoint::register(m)?` call from `#[pymodule]`
- `python/wifi_densepose/__init__.py` — re-exports `Keypoint` and
  `KeypointType` at the package root
- `python/tests/test_keypoint.py` — 23 tests covering:
  - 17-element COCO ordering of `KeypointType.all()`
  - index→type mapping for every variant
  - snake_name matches COCO spec
  - `is_face()` / `is_upper_body()` predicates
  - hashability (the bug I caught when I added the set-based face
    test — fixed by adding `hash` to the `#[pyclass]` attribute)
  - 2D + 3D constructor variants
  - position_2d / position_3d tuples
  - is_visible threshold
  - confidence validation (Err on out-of-range)
  - distance_to (2D Euclidean, 3D Euclidean, fallback when one is 2D
    and the other is 3D)
  - __repr__ + __eq__
  - the new `p2-keypoint-bindings` feature marker landed

## Local validation

\`\`\`
$ cd python && .venv/Scripts/python -m pytest tests/ -v
tests/test_smoke.py::test_package_imports PASSED
tests/test_smoke.py::test_version_string_well_formed PASSED
tests/test_smoke.py::test_rust_version_surfaced PASSED
tests/test_smoke.py::test_build_features_listed PASSED
tests/test_smoke.py::test_hello_returns_ok PASSED
tests/test_smoke.py::test_native_module_private PASSED
tests/test_keypoint.py::test_keypoint_type_all_returns_17 PASSED
…
======================== 29 passed in 0.06s =========================
\`\`\`

Wheel size after both bindings: still well under the 5 MB ADR §5.4
budget (release build with --strip on Windows: ~340 KB).

Also adds `python/.gitignore` to prevent the `.venv/` + `target/` +
`_native.abi3.pyd` artifacts from getting committed.

## What's left in P2

CsiFrame + PoseEstimate bindings land in the next iteration. They're
larger (CsiFrame has the subcarrier buffer; PoseEstimate has
17×Keypoint + BoundingBox + track_id + score). Pattern is now proven
so they go faster.

Refs #785, ADR-117 §6.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 10:54:34 -04:00