mirror of
https://github.com/ruvnet/RuView
synced 2026-06-24 12:43:18 +00:00
cbd24cd1ed
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>
84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
"""WiFi-DensePose — passive human sensing from WiFi CSI.
|
|
|
|
ADR-117 — v2.0 is a PyO3-bound replacement for the legacy pure-Python
|
|
``wifi-densepose==1.1.0`` (released 2025-06-07). The compiled core is
|
|
the same Rust workspace published in `v2/crates/` of the
|
|
`ruvnet/RuView <https://github.com/ruvnet/RuView>`_ repository.
|
|
|
|
Quick start::
|
|
|
|
import wifi_densepose
|
|
print(wifi_densepose.__version__)
|
|
print(wifi_densepose.__rust_version__)
|
|
print(wifi_densepose.hello()) # → "ok"
|
|
|
|
P1 (this release): scaffold. Core types land in P2; vital signs +
|
|
signal DSP in P3; WebSocket/MQTT client in P4. See the
|
|
`ADR-117 modernization plan
|
|
<https://github.com/ruvnet/RuView/blob/main/docs/adr/ADR-117-pip-wifi-densepose-modernization.md>`_
|
|
for the full phase ledger.
|
|
|
|
Migrating from v1.x: the v1 line was pure-Python and had a different
|
|
API surface. v2 is a hard break (semver-justified). See the
|
|
``v1.99.0`` tombstone wheel for the migration URL.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# Public Python version follows the wheel version, NOT the Rust core
|
|
# version. The Rust core version is surfaced separately as
|
|
# `__rust_version__` for diagnostics.
|
|
__version__ = "2.0.0a1"
|
|
|
|
# Re-export the compiled module's surface. The leading underscore on
|
|
# `_native` is intentional — it marks the binding module as internal.
|
|
# Users always import from `wifi_densepose` directly.
|
|
from wifi_densepose import _native
|
|
|
|
# ─── P2 — Core type re-exports ───────────────────────────────────────
|
|
# Bound types land in `wifi_densepose._native` and are re-exported here
|
|
# under their stable public names. Users always `from wifi_densepose
|
|
# import Keypoint, KeypointType` — never reach into `_native`.
|
|
Keypoint = _native.Keypoint
|
|
KeypointType = _native.KeypointType
|
|
BoundingBox = _native.BoundingBox
|
|
PersonPose = _native.PersonPose
|
|
PoseEstimate = _native.PoseEstimate
|
|
|
|
|
|
__rust_version__: str = _native.__rust_version__
|
|
"""Version of the bound Rust core. Useful for bug reports."""
|
|
|
|
__rust_build_tag__: str = _native.__rust_build_tag__
|
|
"""Build tag of the Rust core (P5 will swap this for the git SHA)."""
|
|
|
|
__build_features__: list[str] = list(_native.__build_features__)
|
|
"""Feature flags the wheel was compiled with."""
|
|
|
|
|
|
def hello() -> str:
|
|
"""Smoke test — confirms the compiled module loads and is callable.
|
|
|
|
Returns:
|
|
Always ``"ok"`` if the wheel built and loaded correctly.
|
|
|
|
Used by ``python/tests/test_smoke.py`` to assert the PyO3 round-trip
|
|
works end-to-end on every cibuildwheel target.
|
|
"""
|
|
return _native.hello()
|
|
|
|
|
|
__all__ = [
|
|
"__version__",
|
|
"__rust_version__",
|
|
"__rust_build_tag__",
|
|
"__build_features__",
|
|
"hello",
|
|
# P2 — core types
|
|
"Keypoint",
|
|
"KeypointType",
|
|
"BoundingBox",
|
|
"PersonPose",
|
|
"PoseEstimate",
|
|
]
|