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>
wifi-densepose v2.x — PyO3 bindings for the Rust core
This directory contains the source for the wifi-densepose PyPI wheel
(v2.0+). It's a PyO3 + maturin build that wraps the Rust crates in
v2/crates/ and replaces the legacy pure-Python
wifi-densepose==1.1.0 (released 2025-06-07).
See ADR-117 for the full modernization plan.
Build locally
# Install maturin + dev deps
pip install maturin pytest
# Develop-install — builds the Rust extension in-place
cd python
maturin develop
# Run the smoke tests
pytest tests/
The maturin develop command produces a debug-build wheel installed
into your current Python environment. For release builds:
maturin build --release --strip
The wheel lands under python/target/wheels/.
Layout
python/
├── Cargo.toml # PyO3 + abi3-py310 + Rust deps
├── pyproject.toml # maturin backend + Python metadata
├── README.md # this file
├── src/
│ └── lib.rs # #[pymodule] — Rust binding glue
├── wifi_densepose/ # pure-Python facade (the user-facing API)
│ ├── __init__.py # re-exports compiled module symbols
│ └── py.typed # PEP 561 typed-package marker
└── tests/
└── test_smoke.py # P1 acceptance tests
Phase status (per ADR-117 §6)
- ✅ P1 — Scaffold (this commit): module loads, version constant
exposed, 6 smoke tests pass via
maturin develop. - ⏳ P2 — Core type bindings:
CsiFrame,Keypoint,PoseEstimate. - ⏳ P3 — Vitals + signal DSP: 4-stage HR/BR pipeline +
CsiProcessorPhaseSanitizer, withallow_threadsGIL release on hot loops.
- ⏳ P4 — WS/MQTT client: pure-Python
wifi_densepose.clientextra. - ⏳ P5 — cibuildwheel + PyPI publish: Linux/macOS/Windows × abi3-py310.
Each phase ends with a checkbox PR. Tests are additive — every phase's smoke tests must still pass after later phases land.
Migrating from v1.x
The v1 line was a separate pure-Python implementation. v2 is a hard break (semver-justified by 11.5 months of stack drift). Migration guide ships in docs/migrations/wifi-densepose-1-to-2.md (landing in P5).