Merge pull request #1617 from ruvnet/feat/adr-323-pose-physics

feat: add physics-constrained pose refinement
This commit is contained in:
rUv
2026-08-15 17:08:02 -04:00
committed by GitHub
69 changed files with 9352 additions and 138 deletions

View File

@@ -0,0 +1,364 @@
# ADR-323: Native Rust physics-constrained pose refinement
- **Status**: Proposed
- **Date**: 2026-08-15
- **Deciders**: ruv
- **Owners**: RuView perception and edge runtime maintainers
- **Tags**: pose, physics, rust, uncertainty, provenance, abstention, edge
- **Numbering note**: ADR-323 is the next free number in the authoring checkout. Re-run the ADR index/collision check immediately before merge and rename if needed.
- **Extends**: ADR-020, ADR-027, ADR-079, ADR-101, ADR-135, ADR-145, ADR-150, ADR-273, ADR-279, ADR-282, ADR-295, ADR-296, ADR-297, ADR-298, ADR-302, ADR-303, ADR-304, ADR-305, ADR-306
- **Supersedes**: None
## Executive decision
RuView will add a clean-room native Rust boundary between RF pose inference and
semantic publication. It will preserve the immutable RF observation, publish a
physics assessment, optionally produce a bounded corrected candidate, and
abstain when required evidence is absent. It must never increase observational
confidence merely because a pose is physically plausible.
Three independently gated layers are adopted:
1. A deterministic kinematic auditor and bounded covariance-weighted projector
using Rust and `nalgebra`.
2. An optional articulated-body dynamics auditor using `rapier3d`.
3. A later optional supervised residual model using Burn.
The first production milestone is deterministic audit. It is not a GRIP port,
not PPO, and not evidence that the current pose observer is production-ready.
## Context
ADR-101's committed Cog emits 17 COCO keypoints as normalized 2D coordinates.
Its model has no per-joint uncertainty head and publishes a constant confidence.
The sensing server also contains renderer-oriented EMA and bone clamping. These
surfaces cannot establish metric 3D physics and can make weak evidence look
more convincing.
Pose output can violate bone length, floor, velocity, acceleration, and temporal
continuity constraints. Downstream consumers also cannot reliably distinguish
observed coordinates from derived correction. The rejected premise is:
"physically plausible means more likely correct." Plausibility is only a prior;
many incorrect poses are plausible.
GRIP is architectural inspiration for an observer/controller split, but it
observes four wearable IMUs and pressure insoles and drives a simulator. RuView
observes RF, so GRIP weights are not input-compatible. External code, weights,
simulators, and datasets require independent license review and never enter the
runtime dependency graph by implication.
## Outcome and actors
For every accepted person track/timestamp, the engine returns exactly one
`PoseRefinementV1`, including off, timeout, rejection, and abstention paths:
- immutable `PoseObservationV2` content hash;
- constraint residuals and quality disposition;
- an optional bounded candidate and an explicit `selected` bit;
- a typed reason when correction is unavailable;
- model, calibration, configuration, and optional learned-artifact provenance.
The RF observer owns observations and calibrated uncertainty; tracking owns
identity stability; physics owns assessment/correction only; the sensing server
owns deadlines, modes, publication, and rollback; the evidence engine owns
release evaluation; clients choose raw/both/refined without silent fallback.
## Input and coordinate contract
Metric correction requires a monotonic nanosecond timestamp, session-scoped
track ID, sequence and sensor epoch, 17 ordered COCO joints in metric X/Y/Z,
per-joint positive-semidefinite covariance calibrated on held-out data, a
versioned right-handed Z-up room frame, a normalized upward floor plane, model
and calibration hashes, ADR-302 trust state, and authenticated/replay-protected
source provenance.
`Image2d` observations may be audited for image-plane ratios and continuity but
must never enter 3D projection/dynamics or be called physically corrected.
Unknown trust, missing calibration, missing uncertainty, stale/non-monotonic
input, non-finite values, invalid covariance, excessive tracks, and room-bound
violations fail to raw output with a typed reason.
## Public contracts
`wifi-densepose-core` owns `PoseObservationV2` and `PoseRefinementV1`; no
duplicate server/Cog contract is permitted. Public output remains COCO17. The
engine derives pelvis and thorax virtually and never labels them observed.
The raw content hash is deterministic and excludes its own hash field. The
idempotency key is `(sensor_epoch, sequence, track_id, raw_hash, config_hash)`.
An exact duplicate returns the cached result; same sequence with different
content is a replay rejection.
Contact is `hypothesis` unless a measured sensor and its provenance say
otherwise. Raw, derived, hypothesis, and unknown labels must survive every
projection.
## Confidence invariant
For upstream calibrated confidence `c_obs`, normalized residual `r`, and
normalized intervention `i`:
```text
c_physics = exp(-(beta_r * r + beta_i * i))
c_effective = min(c_obs, c_obs * c_physics)
0 <= c_effective <= c_obs <= 1
```
Only a separately witnessed multimodal fusion contract may increase fused
confidence.
## Deterministic projector
The default `kinematic` feature has no Rapier, Burn, ONNX, libtorch, Python,
CUDA, or network dependency. Per bounded iteration it:
1. projects observed parent/child distances toward anonymous track-scoped
bone-length posteriors;
2. applies broad joint/trunk validity checks without an upright prior;
3. bounds temporal motion and resets derivatives after gaps;
4. resolves floor penetration only, allowing seated, kneeling, prone, child-
scale, mobility-aid, and genuine-fall poses;
5. recomputes residuals and stops below epsilon.
Initial operator-owned caps are four iterations (hard maximum eight), 0.20 m
single-joint correction, 0.10 m root correction, 250 ms derivative gap, 500 ms
track reset, ten known joints, a 100 m metric room bound, a separate 16,384
image-coordinate audit bound, and a 5 ms one-track Pi 5 p95 gate. Keeping image
and metric bounds separate prevents legitimate pixel observations from
weakening the physical room bound. A candidate over either correction cap is
discarded in full.
Bone posteriors are initialized only from high-confidence frames, anonymous,
memory-only, track-scoped, and deleted on expiry. Persistent personalization is
outside this ADR and requires consent/retention/deletion governance.
## Optional dynamics and learned layers
`dynamics` adds a process-owned Rapier humanoid and begins audit-only. Network
input may never provide Rapier snapshots, bodies, constraints, solver limits,
or arbitrary geometry. Dynamics approval is independent of kinematic approval.
`learned` uses first-party Burn 0.21 core/NN components without `burn-tch`
because this workspace already has a different native libtorch link.
`learned-cpu` adds the ndarray backend. The implemented two-layer GRU uses a
20-frame history and width 128 to predict bounded residuals, uncertainty,
foot-contact hypotheses, and abstention. Verified model records can be loaded
from bytes and executed natively; no trained artifact is shipped or approved.
The resolved Burn/CubeCL graph declares Rust 1.92, while the workspace file
pins Rust 1.89 and the authoring host provides Rust 1.91.1.
`--ignore-rust-version` is diagnostic evidence only: learned activation remains
blocked until an approved Rust 1.92 release-toolchain change builds it without
that override. Residuals are hard-clipped to deterministic caps and cannot
bypass validation or confidence monotonicity. PPO is deferred until measured
evidence identifies a failure supervised residual learning cannot address.
## Feature boundary
```text
default = kinematic
dynamics = rapier3d
learned = burn-core + burn-nn
learned-cpu = learned + burn-ndarray
learned-train = learned + burn-train
learned-wgpu = learned-train + burn-wgpu
learned-cuda = learned-train + burn-cuda
deterministic = rapier3d?/enhanced-determinism
```
The lockfile is release authority. The learned feature currently requires the
toolchain supported by Burn/CubeCL's resolved graph; this does not change the
default edge build.
## Runtime modes and API
Rollout is `OFF -> AUDIT -> SHADOW_CORRECT -> OPT_IN_CORRECT -> DEFAULT_CORRECT`.
Evidence permits forward transitions; any regression returns immediately to
audit/off. Correct selection additionally requires authenticated sensor
identity and replay protection from ADR-305. High model confidence cannot
override missing source authentication.
Existing pose fields stay unchanged and raw remains the migration default:
```text
GET /api/v1/pose/current?view=raw
GET /api/v1/pose/current?view=both
GET /api/v1/pose/current?view=refined
```
Refined-only returns HTTP 409 with `pose_refined_unavailable` when no selected
candidate exists. It never silently returns raw labeled refined.
## Security, privacy, and availability
All frames, model output, geometry, and pre-verification artifacts are
untrusted. Calibration/config/model artifacts become trusted only after signed,
hash-addressed verification and atomic activation. Runtime inference performs
no model retrieval or other network access.
Fixed arrays/caps, bounded iterations, a maximum track count, room geometry
limits, deadlines, and track expiry constrain denial of service. Timeout drops
partial refinement, never raw publication. Backpressure retains the newest raw
frame per track, drops intermediate refinement work, resets derivatives after
250 ms, and never extrapolates beyond 500 ms.
Metrics contain only allowlisted aggregate scalars: mode/disposition/reason,
stage latency, iterations, maximum correction, residuals, confidence delta,
track resets, invalid input, timeout, and raw/refined divergence. They exclude
joint arrays, body dimensions, room coordinates, CSI, and persistent person
identifiers. Bone/gait state is memory-only and excluded from logs.
Refined output is not a sole medical, emergency, industrial-safety, or
autonomous-control source. A real fall is valid state and must never be made
upright to stabilize a simulator.
## Threat model summary
| Threat | Primary control | Residual risk |
|---|---|---|
| Spoofed/replayed sensor | ADR-305 identity, MAC, sequence and replay window; correction gate | Compromised legitimate sensor |
| Altered model/floor/config | Signed hashes, authenticated configuration, atomic activation | Authorized unsafe configuration |
| Poisoned data/splits | Immutable manifests, strict split validator, witnessed benchmarks | Subtle label poisoning |
| Operator repudiation | Append-only witnessed transition with actor/old/new hash/reason | Compromised signer |
| Biometric/log leakage | Track-local retention and fixed metric allowlist | Aggregate inference |
| Track/geometry CPU flood | Authentication, cardinality/geometry/allocation/deadline caps | Valid dense-scene overload |
| Remote mode escalation | Capability-scoped local control plane, deny by default | Compromised operator capability |
| Derived output relabeled observed | Required schema/provenance and signed event envelope | Malicious downstream stripping |
The implementation review records commit, lockfile hash, Rust toolchain,
scanner versions, and advisory-feed timestamp.
## Evidence protocol
Evidence levels are L0 deterministic synthetic, L1 public measured replay, L2
controlled RuView RF plus optical truth, L3 subject/room/hardware/session-
disjoint RuView, L4 privacy-safe shadow fleet aggregates, and L5 independent
vertical validation outside this ADR.
No sequence, contiguous take, subject, room, or calibration session may cross
train/test for the generalization gate. Preprocessing, body priors, and
uncertainty calibration fit training data only. Reports include raw observer,
renderer smoothing, audit, deterministic correction, dynamics audit, and
learned residual on identical observations, plus empty-room, prone/fall,
missing-joint, and OOD subsets.
Primary metrics are 3D MPJPE, declared-threshold PCK, per-joint error, foot
slide, floor penetration, jerk, uncertainty calibration, abstention coverage,
and selective risk. Learned runs use at least five fixed seeds and report mean,
median, standard deviation, and 95% bootstrap intervals. All frames count;
selective metrics report risk and coverage.
## Acceptance gates
- **G0 contract**: real metric 3D/covariance output, round-trip raw hash,
versioned frame/floor, 2D compatibility, non-stub observer, ADR-298 artifact
sanity, and the ADR-079 PCK@20 >=35% gate or adopted successor. The current
committed Cog does not pass G0, so correction remains unavailable.
- **G1 deterministic audit**: property/fuzz tests, deterministic hashes per
platform class, 24-hour accelerated replay without panic/growth, Pi 5 p95
<=5 ms, and universal confidence monotonicity.
- **G2 shadow correction**: strict-disjoint measured median MPJPE improvement
>=10% with positive 95% CI lower bound; foot slide >=30% and jerk >=25%
better; no joint median >5 mm worse; fall/prone sensitivity change <=2 pp;
>=95% corrections below 0.10 m; every correction above 0.20 m abstains.
- **G3 opt-in**: >=30 subjects, 10 rooms, 3 hardware configurations, and 3
independent sessions/room; UNKNOWN never selected; confidence monotonic;
live disable; REST/WebSocket/MQTT/Home Assistant/replay compatibility.
- **G4 default visualization only**: 30 shadow days under 0.1% timeout/internal
error, no open severity 1/2 incidents, and gates still valid for current
model/calibration.
Dynamics and learned engines each repeat G2-G4; approval is not inherited.
## Testing and completion evidence
Unit/property/fuzz/integration/security coverage maps to requirements R1-R13:
raw hash, confidence, modes, malformed/stale/frame/covariance input, caps and
deadlines, provenance, dependency graph, pose diversity/fall preservation,
strict splits, fail-to-raw faults, no network capability, and authenticated
source/replay selection.
Release commands include focused core/physics tests, default/dynamics/learned
feature checks, format/clippy, benches, `cargo deny`, `cargo audit`, strict split
verification, and golden replay verification. Completion also requires JSON
schemas, measured Pi 5/x86 rows, strict manifest hashes, raw/refined metrics,
SBOM/license report, rollback drill, and residual-risk owners. Missing measured
or operational evidence leaves status Proposed and runtime in audit.
## Rollback
Rollback is an authenticated mode transition to audit/off, not a binary
downgrade. Stop selection immediately, keep raw publication and disposition
records, discard track state, and retain only aggregate incident metrics plus
signed configuration history. Failed artifact activation leaves the previous
engine atomically active. Additive schemas remain; refined-only callers receive
the typed unavailable response.
## Consequences
### Positive
- Explicit anti-hallucination and provenance boundary after RF inference.
- Reusable native Rust consistency primitive with measurable abstention.
- Python/CUDA remain absent from the production default.
- Cross-modal teacher data remains possible without wearable runtime inputs.
### Negative
- Full value requires a real metric 3D observer and calibrated uncertainty.
- Stateful tracks add latency/memory; optional backends add supply-chain surface.
- A constrained but wrong pose can look more credible.
- Strict data collection costs more than the software implementation.
### Neutral
- This ADR does not improve RF observability or current weight evidence.
- Existing 2D consumers continue to function.
## Implementation phases
P0 contracts/schemas; P1 deterministic audit; P2 bounded shadow correction; P3
server/Cog publication and evidence ledger; P4 Rapier audit; P5 Burn residual
training/inference. Code may land ahead of evidence, but runtime authority
advances only through the gates above.
## Implementation status at proposal
- P0-P3 are implemented on this branch: canonical contracts, strict schemas,
deterministic audit/projection, authenticated correction receipts,
idempotency, bounded track state, latest-frame backpressure, additive HTTP
and WebSocket publication, live legacy-2D audit, privacy-safe metrics, golden
replay, and strict-split checks.
- P4 is implemented as an optional persistent per-track Rapier dynamics auditor
and remains audit-only pending independent G2-G4 evidence.
- P5 inference architecture, artifact verification, serialization, and native
CPU execution are implemented. Training data, a signed trained artifact, and
G2-G4 accuracy/calibration evidence do not exist, so the layer has no runtime
selection authority. Its resolved Rust 1.92 requirement is also an explicit
activation blocker on the current Rust 1.91.1 release host.
- The live Cog honestly emits `Image2d`, degraded trust, and uncalibrated
uncertainty. It can be audited but cannot be selected for 3D correction.
G0 therefore remains open until an independently released metric-3D observer
with calibrated covariance is integrated.
- Local x86 latency and synthetic contract checks are recorded in the append-
only evidence ledger. Pi 5 measurements, 24-hour replay, 100-million-case
fuzzing, held-out RF/optical accuracy, fleet shadowing, and vertical safety
validation remain release evidence gates rather than software claims.
## References
- [GRIP project](https://ryosukehori.github.io/grip-project/)
- [GRIP paper (arXiv:2603.16233)](https://arxiv.org/abs/2603.16233)
- [Rapier documentation](https://docs.rs/rapier3d/)
- [Burn documentation](https://docs.rs/burn/0.21.0/burn/)
- [ADR-020](./ADR-020-rust-ruvector-ai-model-migration.md)
- [ADR-079](./ADR-079-camera-ground-truth-training.md)
- [ADR-101](./ADR-101-pose-estimation-cog.md)
- [ADR-150](./ADR-150-rf-foundation-encoder.md)
- [ADR-273](./ADR-273-unified-rf-spatial-world-model.md)
- [ADR-279](./ADR-279-native-rf-frame-contract.md)
- [ADR-298](./ADR-298-model-release-sanity-gates.md)
- [ADR-302](./ADR-302-out-of-distribution-detection.md)
- [ADR-303](./ADR-303-ground-truth-synchronization.md)
- [ADR-304](./ADR-304-evidence-engine.md)
- [ADR-305](./ADR-305-authenticated-sensor-identity.md)
- [ADR-306](./ADR-306-canonical-spatial-ontology.md)

View File

@@ -179,6 +179,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
| [ADR-319](ADR-319-witness-chain.md) | Witness chain — staged, signed epistemic envelope | Accepted (phase 1) |
| [ADR-320](ADR-320-sensor-hal.md) | RuView sensor HAL — abstract all sensing hardware to one Observation type | Proposed (phase 2) |
| [ADR-321](ADR-321-decision-policy-action-authorization.md) | Decision policy — action authorization conditioned on certificate class, freshness, uncertainty, evidence | Accepted (phase 1) |
| [ADR-323](ADR-323-native-rust-physics-constrained-pose-refinement.md) | Native Rust physics-constrained pose refinement | Proposed |
---

View File

@@ -0,0 +1,71 @@
# Physics pose refinement evidence ledger
ADR-323 performance and accuracy targets are gates, not measured claims. Append
rows; never replace prior measurements. Every row must identify the repository
commit, lockfile hash, Rust toolchain, target, engine/features, configuration
hash, corpus/split hash, command, sample count, and evidence label.
## Runtime measurements
| Date | Commit | Lock SHA-256 | Target/toolchain | Engine/config | Tracks | Samples | p50 | p95 | p99/max | RSS delta | Evidence | Reproducer |
|---|---|---|---|---|---:|---:|---:|---:|---:|---:|---|---|
| 2026-08-15 | `de27336` + uncommitted ADR-323 changes | `552737eab9092b59ea9dd2b2caf68389f0b0966679f0fbb33ff2b1b3d42e2668` | Windows x86_64, Intel Core Ultra 9 285H, rustc 1.91.1 | deterministic kinematic shadow, config `ef3cf581f75124c1d45a8d6bedcef32e4d1bacb39ee0dfcd4e520171fda2d8cf` | 1 | 20,000 | 0.0080 ms | 0.0097 ms | 0.0195/0.5465 ms | not measured | **MEASURED**, local host only; not Pi 5 evidence | `cargo run --release -p wifi-densepose-physics --example latency_probe -- 20000` |
| 2026-08-15 | `de27336` + uncommitted ADR-323 changes | `552737eab9092b59ea9dd2b2caf68389f0b0966679f0fbb33ff2b1b3d42e2668` | Windows x86_64, Intel Core Ultra 9 285H, rustc 1.91.1 | deterministic kinematic shadow after final local optimization, same config | 1 | 20,000 | 0.0075 ms | 0.0084 ms | 0.0117/0.1579 ms | not measured | **MEASURED**, local host only; not Pi 5 evidence | same release probe command |
| 2026-08-15 | `de27336` + uncommitted ADR-323 changes | `552737eab9092b59ea9dd2b2caf68389f0b0966679f0fbb33ff2b1b3d42e2668` | Windows x86_64, Intel Core Ultra 9 285H, rustc 1.91.1 | final deterministic kinematic shadow, config `a44dc696234f31eda54cd4b436bc2d2c69b9638565b729ac9f07435cedfd0dcc` | 1 | 20,000 | 0.0071 ms | 0.0084 ms | 0.0147/1.5994 ms | not measured | **MEASURED**, local host only; not Pi 5 evidence | same release probe command |
The probe measures a warm, one-track `PhysicsEngine::process` call. It excludes
transport, publication, resident-memory delta, dynamics, and learned inference.
It is not evidence for the Pi 5 gate.
Criterion separately measured `kinematic_one_track` at
`[11.911, 12.757, 14.069] us` across 100 samples (approximately 369,000 timed
iterations). That benchmark includes observation construction and canonical
hashing in the timed routine and uses fresh engine state; it is **MEASURED** on
the same local host and is not a percentile or Pi 5 claim.
## Accuracy measurements
| Date | Commit | Corpus/split | Variant | Coverage | MPJPE | PCK threshold/result | Foot slide | Jerk | Fall/prone delta | Evidence |
|---|---|---|---|---:|---:|---|---:|---:|---:|---|
No measured accuracy evidence has been recorded. The deterministic tests are
L0/SYNTHETIC contract evidence only and cannot satisfy G2.
## Validation and supply-chain record
- The default dependency graph is checked to exclude Burn, Rapier, Tch, and
ONNX Runtime. Dynamics and learned backends remain opt-in.
- Burn CPU serialization/inference tests pass on the authoring host only with
Cargo's `--ignore-rust-version`; the resolved CubeCL graph requires Rust 1.92.
The workspace file pins Rust 1.89 and the host provides Rust 1.91.1. This is
diagnostic, not release approval.
- `cargo audit 0.22.1` used RustSec database commit
`69f93cf294852cfa9b53751f4ca86de3283dd290` (feed timestamp 2026-08-12).
ADR-323 updates remove resolved advisories in `event-listener`, `rkyv`, and
`wasmtime`. The workspace still has five advisories in pre-existing
`quick-xml` and `rsa` dependency paths; the default physics graph contains
none of them. The optional Burn training graph includes yanked `spin 0.9.8`.
- `cargo-deny` is not installed on the authoring host, so the required license
and policy gate is not claimed complete.
- Strict Clippy passes with warnings denied for core/physics default and
dynamics builds, the diagnostic learned-CPU build, and the Cog itself with
dependency linting excluded. Focused core, physics, dynamics, learned, Cog,
sensing-server adapter/live-audit/HTTP, schema, golden, strict-split,
feature-boundary, and fuzz-build checks pass.
- The repository-wide rustfmt gate is already red across unrelated crates. The
sensing-server library has existing warning debt, and unscoped Cog Clippy is
blocked by existing `wifi-densepose-ruvector` warnings. The prescribed
`cargo test --workspace --no-default-features` did not reach a terminal result
in either a 904-second cold or 604-second warm serial run on this Windows
host. None of these broader gates is represented as green.
- The standalone fuzz lock SHA-256 is
`d386c4edb130bb6b2d1a4ef77334c78e25e0695e90a9d97c01284876acb8c2c6`.
## Required commands
```text
cargo bench -p wifi-densepose-physics
node scripts/pose-physics/verify-feature-boundary.mjs
bash scripts/verify-pose-physics-splits.sh <manifest.json>
bash scripts/replay-pose-physics-golden.sh <golden-results.jsonl>
```

View File

@@ -0,0 +1,38 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ruview.net/schemas/pose-observation-v2.schema.json",
"title": "PoseObservationV2",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "timestamp_ns", "sensor_epoch", "sequence", "track_id", "frame", "calibration_id", "floor_plane", "model", "source", "trust_state", "dimensionality", "uncertainty_calibrated", "joints", "observer_confidence", "canonical_hash"],
"properties": {
"schema_version": { "const": 2 },
"timestamp_ns": { "type": "integer", "minimum": 0 },
"sensor_epoch": { "type": "integer", "minimum": 0 },
"sequence": { "type": "integer", "minimum": 0 },
"track_id": { "$ref": "#/$defs/string_id" },
"frame": { "$ref": "#/$defs/frame" },
"calibration_id": { "$ref": "#/$defs/string_id" },
"floor_plane": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/floor" }] },
"model": { "$ref": "#/$defs/model" },
"source": { "$ref": "#/$defs/source" },
"trust_state": { "enum": ["KNOWN", "DEGRADED", "UNKNOWN"] },
"dimensionality": { "enum": ["image2d", "metric3d"] },
"uncertainty_calibrated": { "type": "boolean" },
"joints": { "type": "array", "minItems": 17, "maxItems": 17, "items": { "$ref": "#/$defs/joint" } },
"observer_confidence": { "$ref": "#/$defs/probability" },
"canonical_hash": { "$ref": "#/$defs/hash" }
},
"$defs": {
"probability": { "type": "number", "minimum": 0, "maximum": 1 },
"hash": { "type": "array", "minItems": 32, "maxItems": 32, "items": { "type": "integer", "minimum": 0, "maximum": 255 } },
"string_id": { "type": "string", "minLength": 1, "maxLength": 128 },
"vec3": { "type": "array", "minItems": 3, "maxItems": 3, "items": { "type": "number" } },
"frame": { "type": "object", "additionalProperties": false, "required": ["name", "version", "metric", "right_handed", "z_up"], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 128 }, "version": { "type": "integer", "minimum": 1 }, "metric": { "type": "boolean" }, "right_handed": { "type": "boolean" }, "z_up": { "type": "boolean" } } },
"floor": { "type": "object", "additionalProperties": false, "required": ["normal", "offset_m"], "properties": { "normal": { "$ref": "#/$defs/vec3" }, "offset_m": { "type": "number" } } },
"model": { "type": "object", "additionalProperties": false, "required": ["id", "artifact_hash"], "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 128 }, "artifact_hash": { "$ref": "#/$defs/hash" } } },
"source": { "type": "object", "additionalProperties": false, "required": ["sensor_id", "authenticated", "replay_protected"], "properties": { "sensor_id": { "type": "string", "minLength": 1, "maxLength": 128 }, "authenticated": { "type": "boolean" }, "replay_protected": { "type": "boolean" } } },
"covariance": { "type": "object", "additionalProperties": false, "required": ["xx", "xy", "xz", "yy", "yz", "zz"], "properties": { "xx": { "type": "number", "minimum": 0 }, "xy": { "type": "number" }, "xz": { "type": "number" }, "yy": { "type": "number", "minimum": 0 }, "yz": { "type": "number" }, "zz": { "type": "number", "minimum": 0 } } },
"joint": { "type": "object", "additionalProperties": false, "required": ["kind", "position_m", "covariance_m2", "confidence", "visibility"], "properties": { "kind": { "enum": ["nose", "left_eye", "right_eye", "left_ear", "right_ear", "left_shoulder", "right_shoulder", "left_elbow", "right_elbow", "left_wrist", "right_wrist", "left_hip", "right_hip", "left_knee", "right_knee", "left_ankle", "right_ankle"] }, "position_m": { "$ref": "#/$defs/vec3" }, "covariance_m2": { "$ref": "#/$defs/covariance" }, "confidence": { "$ref": "#/$defs/probability" }, "visibility": { "enum": ["visible", "occluded", "unknown"] } } }
}
}

View File

@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ruview.net/schemas/pose-refinement-v1.schema.json",
"title": "PoseRefinementV1",
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "raw_observation_hash", "mode", "disposition", "selected", "refined_joints_m", "physics_confidence", "effective_confidence", "intervention", "residuals", "refined_residuals", "contact_hypotheses", "dynamics", "provenance", "reason", "canonical_hash"],
"properties": {
"schema_version": { "const": 1 },
"raw_observation_hash": { "$ref": "#/$defs/hash" },
"mode": { "enum": ["off", "audit", "shadow_correct", "opt_in_correct", "default_correct"] },
"disposition": { "enum": ["bypassed", "audited2d", "audited", "shadowed", "corrected", "abstained", "rejected"] },
"selected": { "type": "boolean" },
"refined_joints_m": { "oneOf": [{ "type": "null" }, { "type": "array", "minItems": 17, "maxItems": 17, "items": { "$ref": "#/$defs/vec3" } }] },
"physics_confidence": { "$ref": "#/$defs/probability" },
"effective_confidence": { "$ref": "#/$defs/probability" },
"intervention": { "$ref": "#/$defs/intervention" },
"residuals": { "$ref": "#/$defs/residuals" },
"refined_residuals": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/residuals" }] },
"contact_hypotheses": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "$ref": "#/$defs/contact" } },
"dynamics": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/dynamics" }] },
"provenance": { "$ref": "#/$defs/provenance" },
"reason": { "enum": [null, "mode_off", "unsupported_schema", "hash_mismatch", "invalid_number", "invalid_covariance", "stale_input", "coordinate_frame_mismatch", "calibration_unavailable", "uncertainty_uncalibrated", "ood_unknown", "source_unauthenticated", "replay_protection_unavailable", "correction_not_authorized", "replay_rejected", "non_monotonic_input", "too_few_known_joints", "correction_too_large", "deadline_exceeded", "track_capacity", "internal_error"] },
"canonical_hash": { "$ref": "#/$defs/hash" }
},
"$defs": {
"probability": { "type": "number", "minimum": 0, "maximum": 1 },
"hash": { "type": "array", "minItems": 32, "maxItems": 32, "items": { "type": "integer", "minimum": 0, "maximum": 255 } },
"vec3": { "type": "array", "minItems": 3, "maxItems": 3, "items": { "type": "number" } },
"intervention": { "type": "object", "additionalProperties": false, "required": ["max_joint_correction_m", "root_correction_m", "corrected_joint_count", "solver_iterations", "elapsed_us"], "properties": { "max_joint_correction_m": { "type": "number", "minimum": 0 }, "root_correction_m": { "type": "number", "minimum": 0 }, "corrected_joint_count": { "type": "integer", "minimum": 0, "maximum": 17 }, "solver_iterations": { "type": "integer", "minimum": 0, "maximum": 8 }, "elapsed_us": { "type": "integer", "minimum": 0 } } },
"residuals": { "type": "object", "additionalProperties": false, "required": ["bone_m", "joint_limit_rad", "velocity_mps", "acceleration_mps2", "temporal_jerk", "floor_penetration_m", "contact_m", "collision_m", "normalized_total"], "properties": { "bone_m": { "type": "number", "minimum": 0 }, "joint_limit_rad": { "type": "number", "minimum": 0 }, "velocity_mps": { "type": "number", "minimum": 0 }, "acceleration_mps2": { "type": "number", "minimum": 0 }, "temporal_jerk": { "type": "number", "minimum": 0 }, "floor_penetration_m": { "type": "number", "minimum": 0 }, "contact_m": { "type": "number", "minimum": 0 }, "collision_m": { "type": "number", "minimum": 0 }, "normalized_total": { "type": "number", "minimum": 0 } } },
"contact": { "type": "object", "additionalProperties": false, "required": ["state", "probability"], "properties": { "state": { "enum": ["hypothesis", "measured", "unknown"] }, "probability": { "$ref": "#/$defs/probability" } } },
"dynamics": { "type": "object", "additionalProperties": false, "required": ["stable", "segment_count", "joint_count", "contact_count", "substeps", "tracking_error_m", "joint_anchor_error_m", "floor_penetration_m", "control_effort"], "properties": { "stable": { "type": "boolean" }, "segment_count": { "type": "integer", "minimum": 0, "maximum": 255 }, "joint_count": { "type": "integer", "minimum": 0, "maximum": 255 }, "contact_count": { "type": "integer", "minimum": 0, "maximum": 65535 }, "substeps": { "type": "integer", "minimum": 1, "maximum": 8 }, "tracking_error_m": { "type": "number", "minimum": 0 }, "joint_anchor_error_m": { "type": "number", "minimum": 0 }, "floor_penetration_m": { "type": "number", "minimum": 0 }, "control_effort": { "type": "number", "minimum": 0 } } },
"provenance": { "type": "object", "additionalProperties": false, "required": ["engine", "engine_version", "config_hash", "rf_model_hash", "calibration_id", "learned_artifact_hash"], "properties": { "engine": { "type": "string", "minLength": 1, "maxLength": 128 }, "engine_version": { "type": "string", "minLength": 1, "maxLength": 64 }, "config_hash": { "$ref": "#/$defs/hash" }, "rf_model_hash": { "$ref": "#/$defs/hash" }, "calibration_id": { "type": "string", "minLength": 1, "maxLength": 128 }, "learned_artifact_hash": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/hash" }] } } }
}
}

View File

@@ -0,0 +1 @@
{"ankle_penetration_m":0.04,"evidence":"SYNTHETIC/L0","expected_raw_hash":"98caf45c249d584d52ede84e2d76bd8c4bf225d426a4ef1651a5ddb178258b88","expected_result_hash":"8ef2ded432c938055a5ebc86eadff811216884093853f5c740c7aff7708b9bef","sequence":1}

View File

@@ -0,0 +1,21 @@
{
"schema_version": 1,
"records": [
{
"split": "train",
"sequence": "sequence-1",
"take": "take-1",
"subject": "subject-1",
"room": "room-1",
"calibration_session": "calibration-1"
},
{
"split": "test",
"sequence": "sequence-2",
"take": "take-2",
"subject": "subject-1",
"room": "room-2",
"calibration_session": "calibration-2"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"evidence": "SYNTHETIC/L0",
"records": [
{ "split": "train", "sequence": "syn-train-seq", "take": "syn-train-take", "subject": "syn-train-subject", "room": "syn-train-room", "calibration_session": "syn-train-cal" },
{ "split": "validation", "sequence": "syn-validation-seq", "take": "syn-validation-take", "subject": "syn-validation-subject", "room": "syn-validation-room", "calibration_session": "syn-validation-cal" },
{ "split": "test", "sequence": "syn-test-seq", "take": "syn-test-take", "subject": "syn-test-subject", "room": "syn-test-room", "calibration_session": "syn-test-cal" }
]
}

View File

@@ -0,0 +1,25 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const result = spawnSync(
"cargo",
["tree", "-p", "wifi-densepose-physics", "-e", "normal", "--prefix", "none"],
{ cwd: path.join(root, "v2"), encoding: "utf8" },
);
if (result.error) throw result.error;
if (result.status !== 0) {
if (result.stdout) process.stderr.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
process.exit(result.status ?? 1);
}
const forbidden = /^(?:burn(?:-|\s|$)|rapier3d(?:\s|$)|tch(?:\s|$)|ort(?:\s|$))/m;
if (forbidden.test(result.stdout)) {
process.stderr.write(result.stdout);
throw new Error("default physics dependency graph contains an optional heavy backend");
}
process.stdout.write(JSON.stringify({ verdict: "PASS", profile: "default-kinematic" }) + "\n");

View File

@@ -0,0 +1,16 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
const input = process.argv[2];
if (!input) throw new Error("usage: replay-pose-physics-golden.sh <golden-results.jsonl>");
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const result = spawnSync(
"cargo",
["run", "--quiet", "-p", "wifi-densepose-physics", "--features", "deterministic", "--example", "verify_golden", "--", path.resolve(input)],
{ cwd: path.join(root, "v2"), encoding: "utf8" },
);
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
if (result.error) throw result.error;
if (result.status !== 0) process.exit(result.status ?? 1);

View File

@@ -0,0 +1,27 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
const directory = path.dirname(fileURLToPath(import.meta.url));
const run = (script, fixture) =>
spawnSync(process.execPath, [path.join(directory, script), path.join(directory, "testdata", fixture)], {
cwd: path.resolve(directory, "../.."),
encoding: "utf8",
});
const strict = run("verify-splits.mjs", "strict-split-manifest.json");
if (strict.status !== 0 || !strict.stdout.includes('"verdict":"PASS"')) {
throw new Error(`strict split fixture failed: ${strict.stderr || strict.stdout}`);
}
const leaky = run("verify-splits.mjs", "leaky-split-manifest.json");
if (leaky.status === 0 || !leaky.stderr.includes("leakage:")) {
throw new Error("leaky split fixture was not rejected with a typed leakage error");
}
const golden = run("verify-golden.mjs", "golden-results.jsonl");
if (golden.status !== 0 || !golden.stdout.includes('"verdict":"PASS"')) {
throw new Error(`golden replay fixture failed: ${golden.stderr || golden.stdout}`);
}
process.stdout.write(JSON.stringify({ verdict: "PASS", checks: 3 }) + "\n");

View File

@@ -0,0 +1,23 @@
import fs from "node:fs";
import crypto from "node:crypto";
const path = process.argv[2];
if (!path) throw new Error("usage: verify-pose-physics-splits.sh <manifest.json>");
const bytes = fs.readFileSync(path);
const manifest = JSON.parse(bytes);
const dimensions = ["sequence", "take", "subject", "room", "calibration_session"];
if (!Array.isArray(manifest.records) || manifest.records.length === 0) throw new Error("manifest.records must be non-empty");
const ownership = new Map();
for (const record of manifest.records) {
if (!record.split || !["train", "validation", "test"].includes(record.split)) throw new Error("invalid split");
for (const dimension of dimensions) {
const value = record[dimension];
if (typeof value !== "string" || value.length === 0) throw new Error(`missing ${dimension}`);
const key = `${dimension}:${value}`;
const prior = ownership.get(key);
if (prior && prior !== record.split) throw new Error(`leakage: ${key} crosses ${prior}/${record.split}`);
ownership.set(key, record.split);
}
}
const digest = crypto.createHash("sha256").update(bytes).digest("hex");
console.log(JSON.stringify({ verdict: "PASS", records: manifest.records.length, sha256: digest }));

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
node "$(dirname "$0")/pose-physics/verify-golden.mjs" "$@"

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
node "$(dirname "$0")/pose-physics/verify-splits.mjs" "$@"

2472
v2/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
resolver = "2"
members = [
"crates/wifi-densepose-core",
"crates/wifi-densepose-physics", # ADR-323 bounded pose assessment/correction
"crates/wifi-densepose-signal",
"crates/wifi-densepose-nn",
# wifi-densepose-api / -db / -config: removed in #578.
@@ -155,6 +156,16 @@ serde_yaml = "0.9"
tokio = { version = "1.35", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
nalgebra = "0.34"
smallvec = "1.15"
bitflags = "2.9"
rapier3d = { version = "0.33", default-features = false, features = ["std", "dim3", "f32"] }
burn-core = { version = "0.21", default-features = false, features = ["std"] }
burn-nn = { version = "0.21", default-features = false }
burn-ndarray = { version = "0.21", default-features = false, features = ["std"] }
burn-train = { version = "0.21", default-features = false }
burn-wgpu = { version = "0.21", default-features = false, features = ["std"] }
burn-cuda = { version = "0.21", default-features = false, features = ["std"] }
# Signal processing
ndarray = { version = "0.17", features = ["serde"] }
@@ -251,6 +262,7 @@ ruvector-gnn = { version = "2.2.0", default-features = false }
# Internal crates
wifi-densepose-core = { version = "0.3.0", path = "crates/wifi-densepose-core" }
wifi-densepose-physics = { version = "0.1.0", path = "crates/wifi-densepose-physics" }
wifi-densepose-signal = { version = "0.3.0", path = "crates/wifi-densepose-signal" }
wifi-densepose-nn = { version = "0.3.0", path = "crates/wifi-densepose-nn" }
wifi-densepose-api = { version = "0.3.0", path = "crates/wifi-densepose-api" }

View File

@@ -36,6 +36,7 @@ safetensors = "0.4"
# wifi-densepose-train re-exports the model types we need; depend by path
# inside the workspace.
wifi-densepose-train = { version = "0.3.1", path = "../wifi-densepose-train", default-features = false }
wifi-densepose-core = { workspace = true, features = ["serde"] }
[dev-dependencies]
tempfile = "3"

View File

@@ -20,6 +20,11 @@ use candle_core::{DType, Device, Tensor};
use candle_nn::{Conv1d, Conv1dConfig, Linear, Module, VarBuilder};
use std::path::Path;
use std::sync::Arc;
use wifi_densepose_core::{
CalibrationId, Coco17Joint, JointObservation, JointVisibility, ModelRef, PoseDimensionality,
PoseObservationV2, PoseTrustState, Probability, SourceProvenance, SpatialFrameRef,
SymmetricCovariance3, TrackId,
};
/// 56 subcarriers × 20 frames per CSI window — matches the format
/// produced by `scripts/align-ground-truth.js` after #641.
@@ -50,10 +55,87 @@ pub struct PoseOutput {
pub confidence: f32,
}
/// Process-owned provenance required to wrap the current two-dimensional
/// observer output without claiming metric depth or calibrated uncertainty.
#[derive(Debug, Clone)]
pub struct ImageObservationContext {
pub timestamp_ns: u64,
pub sensor_epoch: u64,
pub sequence: u64,
pub model_id: String,
pub model_artifact_hash: [u8; 32],
}
impl PoseOutput {
pub fn is_finite(&self) -> bool {
self.keypoints.iter().all(|v| v.is_finite()) && self.confidence.is_finite()
}
/// Add the canonical ADR-323 contract while honestly retaining the
/// current observer's image-space and uncertainty limitations.
#[must_use]
pub fn to_image_observation(
&self,
context: ImageObservationContext,
) -> Option<PoseObservationV2> {
if self.keypoints.len() != OUTPUT_KEYPOINTS * 2 || !self.is_finite() {
return None;
}
let confidence = Probability::new(self.confidence.clamp(0.0, 1.0)).ok()?;
let joints = core::array::from_fn(|index| JointObservation {
kind: Coco17Joint::ALL[index],
position_m: [self.keypoints[index * 2], self.keypoints[index * 2 + 1], 0.0],
// Zero only means no covariance estimate is supplied. The explicit
// `uncertainty_calibrated=false` gate prevents correction.
covariance_m2: SymmetricCovariance3 {
xx: 0.0,
xy: 0.0,
xz: 0.0,
yy: 0.0,
yz: 0.0,
zz: 0.0,
},
confidence,
visibility: if confidence == Probability::ZERO {
JointVisibility::Unknown
} else {
JointVisibility::Visible
},
});
let mut observation = PoseObservationV2 {
schema_version: wifi_densepose_core::POSE_OBSERVATION_SCHEMA_VERSION,
timestamp_ns: context.timestamp_ns,
sensor_epoch: context.sensor_epoch,
sequence: context.sequence,
track_id: TrackId("local:1".into()),
frame: SpatialFrameRef {
name: "normalized-image".into(),
version: 1,
metric: false,
right_handed: false,
z_up: false,
},
calibration_id: CalibrationId("image:uncalibrated".into()),
floor_plane: None,
model: ModelRef {
id: context.model_id,
artifact_hash: context.model_artifact_hash,
},
source: SourceProvenance {
sensor_id: "sensing-server:loopback".into(),
authenticated: false,
replay_protected: false,
},
trust_state: PoseTrustState::Degraded,
dimensionality: PoseDimensionality::Image2d,
uncertainty_calibrated: false,
joints,
observer_confidence: confidence,
canonical_hash: [0; 32],
};
observation.seal();
Some(observation)
}
}
/// Per-room LoRA calibration adapter (ADR-150 §3.53.6). Low-rank deltas on the pose
@@ -185,6 +267,7 @@ impl PoseNet {
pub struct InferenceEngine {
inner: Option<Arc<LoadedModel>>,
device: Device,
artifact_hash: [u8; 32],
}
struct LoadedModel {
@@ -227,6 +310,7 @@ impl InferenceEngine {
adapter_path: Option<&Path>,
) -> Result<Self, Box<dyn std::error::Error>> {
let device = pick_device();
let artifact_hash = combined_artifact_hash(weights_path, adapter_path)?;
let inner = match weights_path {
Some(p) if p.exists() => {
// SAFETY: `from_mmaped_safetensors` mmaps the file for the
@@ -246,7 +330,11 @@ impl InferenceEngine {
}
_ => None,
};
Ok(Self { inner, device })
Ok(Self {
inner,
device,
artifact_hash,
})
}
/// Whether a per-room calibration adapter is currently attached.
@@ -266,6 +354,13 @@ impl InferenceEngine {
}
}
/// SHA-256 identity of the base weights and optional adapter, or zero for
/// the explicit no-model stub.
#[must_use]
pub const fn artifact_hash(&self) -> [u8; 32] {
self.artifact_hash
}
pub fn infer(&self, window: &CsiWindow) -> Result<PoseOutput, Box<dyn std::error::Error>> {
if window.data.len() != INPUT_SUBCARRIERS * INPUT_TIMESTEPS {
return Err(format!(
@@ -336,6 +431,24 @@ fn pick_device() -> Device {
Device::Cpu
}
fn combined_artifact_hash(
weights_path: Option<&Path>,
adapter_path: Option<&Path>,
) -> Result<[u8; 32], Box<dyn std::error::Error>> {
use sha2::{Digest, Sha256};
let Some(weights_path) = weights_path.filter(|path| path.exists()) else {
return Ok([0; 32]);
};
let mut hasher = Sha256::new();
hasher.update(b"ruview.pose-observer-artifacts-v1\0");
hasher.update(std::fs::read(weights_path)?);
if let Some(adapter_path) = adapter_path.filter(|path| path.exists()) {
hasher.update(b"adapter\0");
hasher.update(std::fs::read(adapter_path)?);
}
Ok(hasher.finalize().into())
}
fn default_weights_path() -> Option<std::path::PathBuf> {
// Search in the order an installed Cog would see it.
let candidates = [

View File

@@ -125,7 +125,10 @@ fn cmd_run(
);
}
let engine = InferenceEngine::with_adapter(adapter.as_deref())?;
let engine = InferenceEngine::with_weights_and_adapter(
Some(cfg.model_path.as_path()),
adapter.as_deref(),
)?;
if engine.is_calibrated() {
tracing::info!("per-room calibration adapter loaded");
}

View File

@@ -42,7 +42,12 @@ impl<'a> Event<'a> {
}
}
pub fn pose_frame(tick: u64, n_persons: usize, persons: Value) -> Self {
pub fn pose_frame(
tick: u64,
n_persons: usize,
persons: Value,
observation: &wifi_densepose_core::PoseObservationV2,
) -> Self {
Self {
ts: now_secs(),
level: "info",
@@ -51,6 +56,7 @@ impl<'a> Event<'a> {
"tick": tick,
"n_persons": n_persons,
"persons": persons,
"observation": observation,
}),
}
}

View File

@@ -2,7 +2,9 @@
//! runs a CSI window through the engine, emits `pose.frame` events.
use crate::config::CogConfig;
use crate::inference::{CsiWindow, InferenceEngine, INPUT_SUBCARRIERS, INPUT_TIMESTEPS};
use crate::inference::{
CsiWindow, ImageObservationContext, InferenceEngine, INPUT_SUBCARRIERS, INPUT_TIMESTEPS,
};
use crate::publisher::{emit_event, Event};
use std::time::Duration;
use tokio::time::sleep;
@@ -13,6 +15,14 @@ pub async fn run_loop(
) -> Result<(), Box<dyn std::error::Error>> {
let mut buffer: Vec<f32> = Vec::with_capacity(INPUT_SUBCARRIERS * INPUT_TIMESTEPS);
let mut tick: u64 = 0;
let process_started = std::time::Instant::now();
let sensor_epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos()
.try_into()
.unwrap_or(u64::MAX);
let model_id = format!("pose-estimation:{}", engine.backend());
let model_artifact_hash = engine.artifact_hash();
loop {
// Poll one frame from the sensing-server. On error, sleep and retry —
@@ -34,7 +44,24 @@ pub async fn run_loop(
"keypoints": chunk_pairs(&out.keypoints),
"confidence": out.confidence,
}]);
emit_event(&Event::pose_frame(tick, 1, persons));
let timestamp_ns = sensor_epoch.saturating_add(
process_started
.elapsed()
.as_nanos()
.try_into()
.unwrap_or(u64::MAX),
);
if let Some(observation) = out.to_image_observation(
ImageObservationContext {
timestamp_ns,
sensor_epoch,
sequence: tick,
model_id: model_id.clone(),
model_artifact_hash,
},
) {
emit_event(&Event::pose_frame(tick, 1, persons, &observation));
}
}
}
}

View File

@@ -5,7 +5,8 @@
use cog_pose_estimation::{
inference::{
InferenceEngine, SyntheticInput, INPUT_SUBCARRIERS, INPUT_TIMESTEPS, OUTPUT_KEYPOINTS,
ImageObservationContext, InferenceEngine, PoseOutput, SyntheticInput, INPUT_SUBCARRIERS,
INPUT_TIMESTEPS, OUTPUT_KEYPOINTS,
},
manifest::ManifestSpec,
};
@@ -108,6 +109,8 @@ fn per_room_adapter_changes_inference_output() {
assert!(!base.is_calibrated(), "base must report uncalibrated");
assert!(cal.is_calibrated(), "adapter engine must report calibrated");
assert_ne!(base.artifact_hash(), [0; 32]);
assert_ne!(base.artifact_hash(), cal.artifact_hash());
// Non-zero input — a zero window would zero the LoRA delta (x·A·B = 0).
let win = cog_pose_estimation::inference::CsiWindow {
@@ -173,6 +176,49 @@ fn manifest_roundtrips() {
assert_eq!(back.version, "0.0.1");
}
#[test]
fn current_observer_is_canonical_image_2d_and_never_claims_calibration() {
let output = PoseOutput {
keypoints: vec![0.5; OUTPUT_KEYPOINTS * 2],
confidence: 0.185,
};
let observation = output
.to_image_observation(ImageObservationContext {
timestamp_ns: 10,
sensor_epoch: 9,
sequence: 1,
model_id: "pose-estimation:test".into(),
model_artifact_hash: [4; 32],
})
.unwrap();
assert_eq!(
observation.dimensionality,
wifi_densepose_core::PoseDimensionality::Image2d
);
assert!(!observation.frame.metric);
assert!(!observation.uncertainty_calibrated);
assert!(!observation.source.authenticated);
assert!(!observation.source.replay_protected);
assert_eq!(observation.canonical_hash, observation.compute_canonical_hash());
}
#[test]
fn malformed_image_pose_cannot_enter_canonical_contract() {
let output = PoseOutput {
keypoints: vec![0.5; 10],
confidence: 0.185,
};
assert!(output
.to_image_observation(ImageObservationContext {
timestamp_ns: 10,
sensor_epoch: 9,
sequence: 1,
model_id: "pose-estimation:test".into(),
model_artifact_hash: [4; 32],
})
.is_none());
}
/// ADR-159 §A1 — the default-config min_confidence threshold must not silently
/// suppress every `pose.frame`. With the old `default_min_confidence()=0.3` and
/// the model's per-frame confidence pinned at 0.185, the runtime gate

View File

@@ -47,12 +47,23 @@
extern crate alloc;
pub mod error;
pub mod pose_observation_v2;
pub mod pose_refinement_v1;
pub mod traits;
pub mod types;
pub mod utils;
// Re-export commonly used types at the crate root
pub use error::{CoreError, CoreResult, InferenceError, SignalError, StorageError};
pub use pose_observation_v2::{
CalibrationId, Coco17Joint, FloorPlane, JointObservation, JointVisibility, ModelRef,
PoseDimensionality, PoseObservationV2, PoseTrustState, Probability, SourceProvenance,
SpatialFrameRef, SymmetricCovariance3, TrackId, POSE_OBSERVATION_SCHEMA_VERSION,
};
pub use pose_refinement_v1::{
AbstentionReason, ConstraintResiduals, ContactHypothesis, ContactState, DynamicsResiduals,
InterventionSummary, PhysicsMode, PhysicsProvenance, PoseRefinementV1, RefinementDisposition,
};
pub use traits::{CanonicalFrame, DataStore, NeuralInference, SignalProcessor};
pub use types::{
AntennaConfig,

View File

@@ -0,0 +1,401 @@
//! Canonical metric pose observation contract for physics assessment (ADR-323).
#![allow(missing_docs)]
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(feature = "std")]
use std::string::String;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Schema version accepted by [`PoseObservationV2`].
pub const POSE_OBSERVATION_SCHEMA_VERSION: u16 = 2;
/// A validated probability in the inclusive range `[0, 1]`.
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "f32", into = "f32"))]
pub struct Probability(f32);
impl Probability {
/// Construct a finite probability.
///
/// # Errors
///
/// Returns an error for non-finite values or values outside `[0, 1]`.
pub fn new(value: f32) -> Result<Self, &'static str> {
if value.is_finite() && (0.0..=1.0).contains(&value) {
Ok(Self(value))
} else {
Err("probability must be finite and in [0, 1]")
}
}
/// Return the underlying value.
#[must_use]
pub const fn get(self) -> f32 {
self.0
}
/// Zero probability.
pub const ZERO: Self = Self(0.0);
/// Unit probability.
pub const ONE: Self = Self(1.0);
}
impl TryFrom<f32> for Probability {
type Error = &'static str;
fn try_from(value: f32) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<Probability> for f32 {
fn from(value: Probability) -> Self {
value.get()
}
}
macro_rules! string_id {
($name:ident) => {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct $name(pub String);
};
}
string_id!(TrackId);
string_id!(CalibrationId);
/// Versioned coordinate frame. Metric correction requires every boolean here.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SpatialFrameRef {
/// Stable frame name.
pub name: String,
/// Frame definition version.
pub version: u32,
/// Coordinates are metres rather than image-normalized units.
pub metric: bool,
/// The axes form a right-handed system.
pub right_handed: bool,
/// Positive Z is vertical/up.
pub z_up: bool,
}
/// Signed model identity.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ModelRef {
/// Human-readable model/version identifier.
pub id: String,
/// Verified model artifact digest.
pub artifact_hash: [u8; 32],
}
/// Sensor provenance relevant to correction selection.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SourceProvenance {
/// Stable sensor identity.
pub sensor_id: String,
/// True only after message/source authentication succeeds.
pub authenticated: bool,
/// True only when sequence binding and replay-window checks are active.
pub replay_protected: bool,
}
/// RF distribution trust state from ADR-302.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum PoseTrustState {
/// Calibration/evidence says the sample is in distribution.
Known,
/// Input is usable for audit but confidence should be reduced.
Degraded,
/// Input is out-of-distribution or cannot be classified.
Unknown,
}
/// Whether the observation is image-plane or physical 3D.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum PoseDimensionality {
/// Normalized or pixel image coordinates; audit-only.
Image2d,
/// Metric room-frame X/Y/Z coordinates.
Metric3d,
}
/// Normalized plane `normal dot point + offset_m = 0`.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FloorPlane {
/// Upward-pointing unit normal.
pub normal: [f32; 3],
/// Plane offset in metres.
pub offset_m: f32,
}
impl FloorPlane {
/// Whether all values are finite and the normal is approximately unit length.
#[must_use]
pub fn is_valid(self) -> bool {
if !self.normal.iter().all(|v| v.is_finite()) || !self.offset_m.is_finite() {
return false;
}
let n2 = self.normal.iter().map(|v| v * v).sum::<f32>();
(n2 - 1.0).abs() <= 1.0e-3 && self.normal[2] > 0.0
}
/// Signed distance to the plane in metres.
#[must_use]
pub fn signed_distance(self, point: [f32; 3]) -> f32 {
self.normal[0].mul_add(
point[0],
self.normal[1].mul_add(point[1], self.normal[2].mul_add(point[2], self.offset_m)),
)
}
}
/// The six unique entries of a symmetric 3x3 covariance matrix.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SymmetricCovariance3 {
pub xx: f32,
pub xy: f32,
pub xz: f32,
pub yy: f32,
pub yz: f32,
pub zz: f32,
}
impl SymmetricCovariance3 {
/// Conservative positive-semidefinite validation using principal minors.
#[must_use]
#[allow(
clippy::similar_names,
clippy::suboptimal_flops,
clippy::suspicious_operation_groupings
)]
pub fn is_positive_semidefinite(self) -> bool {
const EPS: f32 = 1.0e-8;
let values = [self.xx, self.xy, self.xz, self.yy, self.yz, self.zz];
if !values.iter().all(|v| v.is_finite()) {
return false;
}
if self.xx < -EPS || self.yy < -EPS || self.zz < -EPS {
return false;
}
let xy_minor = self.xx * self.yy - self.xy * self.xy;
let xz_minor = self.xx * self.zz - self.xz * self.xz;
let yz_minor = self.yy * self.zz - self.yz * self.yz;
let det = self.xx * (self.yy * self.zz - self.yz * self.yz)
- self.xy * (self.xy * self.zz - self.yz * self.xz)
+ self.xz * (self.xy * self.yz - self.yy * self.xz);
xy_minor >= -EPS && xz_minor >= -EPS && yz_minor >= -EPS && det >= -EPS
}
/// Trace, used as a bounded uncertainty weight.
#[must_use]
pub fn trace(self) -> f32 {
self.xx + self.yy + self.zz
}
}
/// COCO-17 joint identity in canonical order.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[repr(u8)]
pub enum Coco17Joint {
Nose,
LeftEye,
RightEye,
LeftEar,
RightEar,
LeftShoulder,
RightShoulder,
LeftElbow,
RightElbow,
LeftWrist,
RightWrist,
LeftHip,
RightHip,
LeftKnee,
RightKnee,
LeftAnkle,
RightAnkle,
}
impl Coco17Joint {
/// Canonical COCO-17 order.
pub const ALL: [Self; 17] = [
Self::Nose,
Self::LeftEye,
Self::RightEye,
Self::LeftEar,
Self::RightEar,
Self::LeftShoulder,
Self::RightShoulder,
Self::LeftElbow,
Self::RightElbow,
Self::LeftWrist,
Self::RightWrist,
Self::LeftHip,
Self::RightHip,
Self::LeftKnee,
Self::RightKnee,
Self::LeftAnkle,
Self::RightAnkle,
];
}
/// Whether a joint is usable by the observer.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum JointVisibility {
Visible,
Occluded,
Unknown,
}
/// One observed COCO joint and calibrated uncertainty.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct JointObservation {
pub kind: Coco17Joint,
pub position_m: [f32; 3],
pub covariance_m2: SymmetricCovariance3,
pub confidence: Probability,
pub visibility: JointVisibility,
}
/// Immutable pose observation presented to the physics boundary.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PoseObservationV2 {
pub schema_version: u16,
pub timestamp_ns: u64,
pub sensor_epoch: u64,
pub sequence: u64,
pub track_id: TrackId,
pub frame: SpatialFrameRef,
pub calibration_id: CalibrationId,
pub floor_plane: Option<FloorPlane>,
pub model: ModelRef,
pub source: SourceProvenance,
pub trust_state: PoseTrustState,
pub dimensionality: PoseDimensionality,
pub uncertainty_calibrated: bool,
pub joints: [JointObservation; 17],
pub observer_confidence: Probability,
pub canonical_hash: [u8; 32],
}
impl PoseObservationV2 {
/// Compute a deterministic content hash excluding `canonical_hash` itself.
#[must_use]
pub fn compute_canonical_hash(&self) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(b"ruview.pose-observation-v2\0");
h.update(&self.schema_version.to_le_bytes());
h.update(&self.timestamp_ns.to_le_bytes());
h.update(&self.sensor_epoch.to_le_bytes());
h.update(&self.sequence.to_le_bytes());
hash_str(&mut h, &self.track_id.0);
hash_str(&mut h, &self.frame.name);
h.update(&self.frame.version.to_le_bytes());
h.update(&[
self.frame.metric.into(),
self.frame.right_handed.into(),
self.frame.z_up.into(),
]);
hash_str(&mut h, &self.calibration_id.0);
match self.floor_plane {
Some(plane) => {
h.update(&[1]);
for value in plane.normal {
h.update(&value.to_bits().to_le_bytes());
}
h.update(&plane.offset_m.to_bits().to_le_bytes());
}
None => {
h.update(&[0]);
}
}
hash_str(&mut h, &self.model.id);
h.update(&self.model.artifact_hash);
hash_str(&mut h, &self.source.sensor_id);
h.update(&[
self.source.authenticated.into(),
self.source.replay_protected.into(),
]);
h.update(&[
self.trust_state as u8,
self.dimensionality as u8,
self.uncertainty_calibrated.into(),
]);
for joint in &self.joints {
h.update(&[joint.kind as u8, joint.visibility as u8]);
for value in joint.position_m {
h.update(&value.to_bits().to_le_bytes());
}
for value in [
joint.covariance_m2.xx,
joint.covariance_m2.xy,
joint.covariance_m2.xz,
joint.covariance_m2.yy,
joint.covariance_m2.yz,
joint.covariance_m2.zz,
] {
h.update(&value.to_bits().to_le_bytes());
}
h.update(&joint.confidence.get().to_bits().to_le_bytes());
}
h.update(&self.observer_confidence.get().to_bits().to_le_bytes());
*h.finalize().as_bytes()
}
/// Set `canonical_hash` to the deterministic content hash.
pub fn seal(&mut self) {
self.canonical_hash = self.compute_canonical_hash();
}
}
fn hash_str(hasher: &mut blake3::Hasher, value: &str) {
let bytes = value.as_bytes();
hasher.update(&(bytes.len() as u64).to_le_bytes());
hasher.update(bytes);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probability_rejects_non_finite_and_out_of_range() {
assert!(Probability::new(f32::NAN).is_err());
assert!(Probability::new(-0.1).is_err());
assert!(Probability::new(1.1).is_err());
assert!((Probability::new(0.5).unwrap().get() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn covariance_rejects_negative_principal_minor() {
let covariance = SymmetricCovariance3 {
xx: 0.1,
xy: 1.0,
xz: 0.0,
yy: 0.1,
yz: 0.0,
zz: 0.1,
};
assert!(!covariance.is_positive_semidefinite());
}
}

View File

@@ -0,0 +1,317 @@
//! Additive physics assessment and correction contract (ADR-323).
#![allow(missing_docs)]
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(feature = "std")]
use std::string::String;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::Probability;
/// Runtime rollout mode.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum PhysicsMode {
Off,
Audit,
ShadowCorrect,
OptInCorrect,
DefaultCorrect,
}
impl PhysicsMode {
/// Whether a validated candidate may be selected for downstream use.
#[must_use]
pub const fn selects_correction(self) -> bool {
matches!(self, Self::OptInCorrect | Self::DefaultCorrect)
}
}
/// Outcome of processing exactly one accepted frame.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum RefinementDisposition {
Bypassed,
Audited2d,
Audited,
Shadowed,
Corrected,
Abstained,
Rejected,
}
/// Typed fail-to-raw reason.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum AbstentionReason {
ModeOff,
UnsupportedSchema,
HashMismatch,
InvalidNumber,
InvalidCovariance,
StaleInput,
CoordinateFrameMismatch,
CalibrationUnavailable,
UncertaintyUncalibrated,
OodUnknown,
SourceUnauthenticated,
ReplayProtectionUnavailable,
CorrectionNotAuthorized,
ReplayRejected,
NonMonotonicInput,
TooFewKnownJoints,
CorrectionTooLarge,
DeadlineExceeded,
TrackCapacity,
InternalError,
}
/// Constraint residuals. Values are metric or normalized as named.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ConstraintResiduals {
pub bone_m: f32,
pub joint_limit_rad: f32,
pub velocity_mps: f32,
pub acceleration_mps2: f32,
pub temporal_jerk: f32,
pub floor_penetration_m: f32,
pub contact_m: f32,
pub collision_m: f32,
pub normalized_total: f32,
}
/// Summary of how much the candidate differs from the observation.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct InterventionSummary {
pub max_joint_correction_m: f32,
pub root_correction_m: f32,
pub corrected_joint_count: u8,
pub solver_iterations: u8,
pub elapsed_us: u64,
}
/// Optional articulated-body audit scalars.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DynamicsResiduals {
pub stable: bool,
pub segment_count: u8,
pub joint_count: u8,
pub contact_count: u16,
pub substeps: u8,
pub tracking_error_m: f32,
pub joint_anchor_error_m: f32,
pub floor_penetration_m: f32,
pub control_effort: f32,
}
/// Contact state is never promoted to measured without sensor provenance.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum ContactState {
Hypothesis,
Measured,
Unknown,
}
/// One foot-contact assessment.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ContactHypothesis {
pub state: ContactState,
pub probability: Probability,
}
impl Default for ContactHypothesis {
fn default() -> Self {
Self {
state: ContactState::Unknown,
probability: Probability::ZERO,
}
}
}
/// Version and artifact lineage for an assessment.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PhysicsProvenance {
pub engine: String,
pub engine_version: String,
pub config_hash: [u8; 32],
pub rf_model_hash: [u8; 32],
pub calibration_id: String,
pub learned_artifact_hash: Option<[u8; 32]>,
}
/// Physics result returned for every accepted input frame.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PoseRefinementV1 {
pub schema_version: u16,
pub raw_observation_hash: [u8; 32],
pub mode: PhysicsMode,
pub disposition: RefinementDisposition,
pub selected: bool,
pub refined_joints_m: Option<[[f32; 3]; 17]>,
pub physics_confidence: Probability,
pub effective_confidence: Probability,
pub intervention: InterventionSummary,
/// Residuals of the immutable observer output.
pub residuals: ConstraintResiduals,
/// Residuals of the bounded candidate, when a candidate was computed.
pub refined_residuals: Option<ConstraintResiduals>,
/// Present only when the separately gated Rapier auditor ran.
pub dynamics: Option<DynamicsResiduals>,
pub contact_hypotheses: [ContactHypothesis; 2],
pub provenance: PhysicsProvenance,
pub reason: Option<AbstentionReason>,
/// Canonical BLAKE3 hash of every preceding result field.
pub canonical_hash: [u8; 32],
}
impl PoseRefinementV1 {
/// Compute a deterministic content hash excluding `canonical_hash` itself.
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn compute_canonical_hash(&self) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(b"ruview.pose-refinement-v1\0");
h.update(&self.schema_version.to_le_bytes());
h.update(&self.raw_observation_hash);
h.update(&[
self.mode as u8,
self.disposition as u8,
self.selected.into(),
]);
match self.refined_joints_m {
Some(joints) => {
h.update(&[1]);
for joint in joints {
hash_f32s(&mut h, &joint);
}
}
None => {
h.update(&[0]);
}
}
hash_f32s(
&mut h,
&[
self.physics_confidence.get(),
self.effective_confidence.get(),
],
);
hash_f32s(
&mut h,
&[
self.intervention.max_joint_correction_m,
self.intervention.root_correction_m,
],
);
h.update(&[
self.intervention.corrected_joint_count,
self.intervention.solver_iterations,
]);
h.update(&self.intervention.elapsed_us.to_le_bytes());
hash_residuals(&mut h, self.residuals);
match self.refined_residuals {
Some(residuals) => {
h.update(&[1]);
hash_residuals(&mut h, residuals);
}
None => {
h.update(&[0]);
}
}
match self.dynamics {
Some(dynamics) => {
h.update(&[1]);
h.update(&[
dynamics.stable.into(),
dynamics.segment_count,
dynamics.joint_count,
dynamics.substeps,
]);
h.update(&dynamics.contact_count.to_le_bytes());
hash_f32s(
&mut h,
&[
dynamics.tracking_error_m,
dynamics.joint_anchor_error_m,
dynamics.floor_penetration_m,
dynamics.control_effort,
],
);
}
None => {
h.update(&[0]);
}
}
for contact in self.contact_hypotheses {
h.update(&[contact.state as u8]);
hash_f32s(&mut h, &[contact.probability.get()]);
}
hash_str(&mut h, &self.provenance.engine);
hash_str(&mut h, &self.provenance.engine_version);
h.update(&self.provenance.config_hash);
h.update(&self.provenance.rf_model_hash);
hash_str(&mut h, &self.provenance.calibration_id);
match self.provenance.learned_artifact_hash {
Some(hash) => {
h.update(&[1]);
h.update(&hash);
}
None => {
h.update(&[0]);
}
}
match self.reason {
Some(reason) => h.update(&[1, reason as u8]),
None => h.update(&[0]),
};
*h.finalize().as_bytes()
}
/// Seal the result after all fields have been populated.
pub fn seal(&mut self) {
self.canonical_hash = self.compute_canonical_hash();
}
}
fn hash_residuals(hasher: &mut blake3::Hasher, residuals: ConstraintResiduals) {
hash_f32s(
hasher,
&[
residuals.bone_m,
residuals.joint_limit_rad,
residuals.velocity_mps,
residuals.acceleration_mps2,
residuals.temporal_jerk,
residuals.floor_penetration_m,
residuals.contact_m,
residuals.collision_m,
residuals.normalized_total,
],
);
}
fn hash_f32s(hasher: &mut blake3::Hasher, values: &[f32]) {
for value in values {
hasher.update(&value.to_bits().to_le_bytes());
}
}
fn hash_str(hasher: &mut blake3::Hasher, value: &str) {
hasher.update(&(value.len() as u64).to_le_bytes());
hasher.update(value.as_bytes());
}

View File

@@ -0,0 +1,58 @@
[package]
name = "wifi-densepose-physics"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description = "Bounded, provenance-preserving pose physics assessment for RuView"
[features]
default = ["kinematic"]
kinematic = []
dynamics = ["dep:rapier3d"]
learned = ["dep:burn-core", "dep:burn-nn"]
learned-cpu = ["learned", "dep:burn-ndarray"]
learned-train = ["learned", "dep:burn-train"]
learned-wgpu = ["learned-train", "dep:burn-wgpu"]
learned-cuda = ["learned-train", "dep:burn-cuda"]
deterministic = ["rapier3d?/enhanced-determinism"]
[dependencies]
wifi-densepose-core = { workspace = true, features = ["serde"] }
nalgebra.workspace = true
serde.workspace = true
thiserror.workspace = true
smallvec.workspace = true
bitflags.workspace = true
tracing.workspace = true
blake3 = "1.5"
rapier3d = { workspace = true, optional = true }
burn-core = { workspace = true, optional = true }
burn-nn = { workspace = true, optional = true }
burn-ndarray = { workspace = true, optional = true }
burn-train = { workspace = true, optional = true }
burn-wgpu = { workspace = true, optional = true }
burn-cuda = { workspace = true, optional = true }
[dev-dependencies]
serde_json.workspace = true
proptest.workspace = true
criterion.workspace = true
[[bench]]
name = "frame_latency"
harness = false
[lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
[lints.clippy]
all = "warn"
pedantic = "warn"
missing_errors_doc = "allow"
module_name_repetitions = "allow"
cast_precision_loss = "allow"
cast_possible_truncation = "allow"
cast_sign_loss = "allow"

View File

@@ -0,0 +1,84 @@
#![allow(missing_docs)]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use wifi_densepose_core::{
CalibrationId, Coco17Joint, FloorPlane, JointObservation, JointVisibility, ModelRef,
PhysicsMode, PoseDimensionality, PoseObservationV2, PoseTrustState, Probability,
SourceProvenance, SpatialFrameRef, SymmetricCovariance3, TrackId,
};
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
fn observation(sequence: u64) -> PoseObservationV2 {
let joints = Coco17Joint::ALL.map(|kind| JointObservation {
kind,
position_m: [f32::from(kind as u8) * 0.01, 0.0, 1.0],
covariance_m2: SymmetricCovariance3 {
xx: 0.001,
xy: 0.0,
xz: 0.0,
yy: 0.001,
yz: 0.0,
zz: 0.001,
},
confidence: Probability::new(0.8).unwrap(),
visibility: JointVisibility::Visible,
});
let mut raw = PoseObservationV2 {
schema_version: 2,
timestamp_ns: 1_000_000_000 + sequence * 33_000_000,
sensor_epoch: 1,
sequence,
track_id: TrackId("bench".into()),
frame: SpatialFrameRef {
name: "room".into(),
version: 1,
metric: true,
right_handed: true,
z_up: true,
},
calibration_id: CalibrationId("cal".into()),
floor_plane: Some(FloorPlane {
normal: [0.0, 0.0, 1.0],
offset_m: 0.0,
}),
model: ModelRef {
id: "bench".into(),
artifact_hash: [1; 32],
},
source: SourceProvenance {
sensor_id: "bench".into(),
authenticated: true,
replay_protected: true,
},
trust_state: PoseTrustState::Known,
dimensionality: PoseDimensionality::Metric3d,
uncertainty_calibrated: true,
joints,
observer_confidence: Probability::new(0.8).unwrap(),
canonical_hash: [0; 32],
};
raw.seal();
raw
}
fn bench_frame(c: &mut Criterion) {
c.bench_function("kinematic_one_track", |b| {
b.iter_batched(
|| {
PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::ShadowCorrect,
..PhysicsConfig::default()
})
.unwrap()
},
|mut engine| {
let raw = observation(1);
black_box(engine.process(&raw, raw.timestamp_ns));
},
criterion::BatchSize::SmallInput,
);
});
}
criterion_group!(benches, bench_frame);
criterion_main!(benches);

View File

@@ -0,0 +1,130 @@
//! Reproducible release-mode latency probe. Results apply only to this host.
use std::{fmt::Write as _, hint::black_box, time::Instant};
use wifi_densepose_core::{
CalibrationId, Coco17Joint, FloorPlane, JointObservation, JointVisibility, ModelRef,
PhysicsMode, PoseDimensionality, PoseObservationV2, PoseTrustState, Probability,
SourceProvenance, SpatialFrameRef, SymmetricCovariance3, TrackId,
};
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let samples = std::env::args()
.nth(1)
.map_or(Ok(20_000usize), |value| value.parse())?;
if samples < 100 {
return Err("sample count must be at least 100".into());
}
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::ShadowCorrect,
..PhysicsConfig::default()
})?;
let config_hash = encode_hex(engine.config_hash());
for sequence in 1..=1_000 {
let raw = observation(sequence);
black_box(engine.process(&raw, raw.timestamp_ns));
}
let mut nanoseconds = Vec::with_capacity(samples);
for offset in 0..samples {
let sequence = 1_001 + offset as u64;
let raw = observation(sequence);
let started = Instant::now();
let result = engine.process(&raw, raw.timestamp_ns);
nanoseconds.push(started.elapsed().as_nanos() as u64);
assert!(result.effective_confidence.get() <= raw.observer_confidence.get());
black_box(result);
}
nanoseconds.sort_unstable();
let percentile =
|numerator: usize| nanoseconds[((samples - 1) * numerator) / 100] as f64 / 1_000_000.0;
println!(
"{{\"evidence\":\"MEASURED\",\"scope\":\"local-host-only\",\"engine\":\"kinematic-pbd\",\"mode\":\"shadow_correct\",\"config_hash\":\"{config_hash}\",\"samples\":{samples},\"p50_ms\":{:.6},\"p95_ms\":{:.6},\"p99_ms\":{:.6},\"max_ms\":{:.6}}}",
percentile(50),
percentile(95),
percentile(99),
nanoseconds[samples - 1] as f64 / 1_000_000.0,
);
Ok(())
}
fn encode_hex(bytes: [u8; 32]) -> String {
let mut encoded = String::with_capacity(64);
for byte in bytes {
write!(&mut encoded, "{byte:02x}").expect("writing to String cannot fail");
}
encoded
}
fn observation(sequence: u64) -> PoseObservationV2 {
let positions = [
[0.0, 0.0, 1.70],
[-0.03, 0.0, 1.73],
[0.03, 0.0, 1.73],
[-0.08, 0.0, 1.71],
[0.08, 0.0, 1.71],
[-0.20, 0.0, 1.45],
[0.20, 0.0, 1.45],
[-0.35, 0.0, 1.15],
[0.35, 0.0, 1.15],
[-0.45, 0.0, 0.90],
[0.45, 0.0, 0.90],
[-0.14, 0.0, 0.90],
[0.14, 0.0, 0.90],
[-0.14, 0.0, 0.48],
[0.14, 0.0, 0.48],
[-0.14, 0.0, 0.04],
[0.14, 0.0, 0.04],
];
let mut joints = core::array::from_fn(|index| JointObservation {
kind: Coco17Joint::ALL[index],
position_m: positions[index],
covariance_m2: SymmetricCovariance3 {
xx: 0.001,
xy: 0.0,
xz: 0.0,
yy: 0.001,
yz: 0.0,
zz: 0.001,
},
confidence: Probability::new(0.8).expect("fixture confidence is valid"),
visibility: JointVisibility::Visible,
});
joints[15].position_m[2] -= (sequence % 7) as f32 * 0.001;
let mut raw = PoseObservationV2 {
schema_version: 2,
timestamp_ns: 1_000_000_000 + sequence * 33_000_000,
sensor_epoch: 1,
sequence,
track_id: TrackId("latency:1".into()),
frame: SpatialFrameRef {
name: "room:latency".into(),
version: 1,
metric: true,
right_handed: true,
z_up: true,
},
calibration_id: CalibrationId("cal:latency".into()),
floor_plane: Some(FloorPlane {
normal: [0.0, 0.0, 1.0],
offset_m: 0.0,
}),
model: ModelRef {
id: "pose:latency".into(),
artifact_hash: [1; 32],
},
source: SourceProvenance {
sensor_id: "sensor:latency".into(),
authenticated: false,
replay_protected: false,
},
trust_state: PoseTrustState::Known,
dimensionality: PoseDimensionality::Metric3d,
uncertainty_calibrated: true,
joints,
observer_confidence: Probability::new(0.78).expect("fixture confidence is valid"),
canonical_hash: [0; 32],
};
raw.seal();
raw
}

View File

@@ -0,0 +1,154 @@
//! Deterministic L0 replay verifier for ADR-323 golden hashes.
use std::{fmt::Write as _, path::Path};
use wifi_densepose_core::{
CalibrationId, Coco17Joint, FloorPlane, JointObservation, JointVisibility, ModelRef,
PoseDimensionality, PoseObservationV2, PoseTrustState, Probability, SourceProvenance,
SpatialFrameRef, SymmetricCovariance3, TrackId,
};
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut arguments = std::env::args().skip(1);
let path = arguments
.next()
.ok_or("usage: verify_golden <golden.jsonl> [--emit|--contracts]")?;
let mode = arguments.next();
let emit = mode.as_deref() == Some("--emit");
let contracts = mode.as_deref() == Some("--contracts");
if mode.is_some() && !emit && !contracts {
return Err("mode must be --emit or --contracts".into());
}
let contents = std::fs::read_to_string(Path::new(&path))?;
let mut count = 0usize;
for (line_index, line) in contents
.lines()
.filter(|line| !line.trim().is_empty())
.enumerate()
{
let mut value: serde_json::Value = serde_json::from_str(line)?;
let sequence = value["sequence"].as_u64().ok_or("sequence must be u64")?;
let penetration = value["ankle_penetration_m"]
.as_f64()
.ok_or("ankle_penetration_m must be numeric")? as f32;
if value["evidence"] != "SYNTHETIC/L0" {
return Err(format!("line {}: evidence must be SYNTHETIC/L0", line_index + 1).into());
}
let mut raw = observation(sequence);
raw.joints[15].position_m[2] -= penetration;
raw.seal();
let result = PhysicsEngine::new(PhysicsConfig::default())?.process(&raw, raw.timestamp_ns);
let raw_hash = encode_hex(raw.canonical_hash);
let result_hash = encode_hex(result.canonical_hash);
if emit {
value["expected_raw_hash"] = raw_hash.clone().into();
value["expected_result_hash"] = result_hash.clone().into();
println!("{}", serde_json::to_string(&value)?);
} else {
if value["expected_raw_hash"] != raw_hash
|| value["expected_result_hash"] != result_hash
{
return Err(format!(
"line {}: hash mismatch (raw={raw_hash}, result={result_hash})",
line_index + 1
)
.into());
}
if contracts {
println!(
"{}",
serde_json::to_string(&serde_json::json!({ "raw": raw, "result": result }))?
);
}
}
count += 1;
}
if count == 0 {
return Err("golden replay is empty".into());
}
if !emit && !contracts {
println!("{{\"verdict\":\"PASS\",\"records\":{count},\"engine\":\"kinematic-pbd\",\"evidence\":\"SYNTHETIC/L0\"}}");
}
Ok(())
}
fn observation(sequence: u64) -> PoseObservationV2 {
let positions = [
[0.0, 0.0, 1.70],
[-0.03, 0.0, 1.73],
[0.03, 0.0, 1.73],
[-0.08, 0.0, 1.71],
[0.08, 0.0, 1.71],
[-0.20, 0.0, 1.45],
[0.20, 0.0, 1.45],
[-0.35, 0.0, 1.15],
[0.35, 0.0, 1.15],
[-0.45, 0.0, 0.90],
[0.45, 0.0, 0.90],
[-0.14, 0.0, 0.90],
[0.14, 0.0, 0.90],
[-0.14, 0.0, 0.48],
[0.14, 0.0, 0.48],
[-0.14, 0.0, 0.04],
[0.14, 0.0, 0.04],
];
let joints = core::array::from_fn(|index| JointObservation {
kind: Coco17Joint::ALL[index],
position_m: positions[index],
covariance_m2: SymmetricCovariance3 {
xx: 0.001,
xy: 0.0,
xz: 0.0,
yy: 0.001,
yz: 0.0,
zz: 0.001,
},
confidence: Probability::new(0.8).expect("fixture confidence is valid"),
visibility: JointVisibility::Visible,
});
let mut raw = PoseObservationV2 {
schema_version: 2,
timestamp_ns: 1_000_000_000 + sequence * 33_000_000,
sensor_epoch: 7,
sequence,
track_id: TrackId("golden:1".into()),
frame: SpatialFrameRef {
name: "room:golden".into(),
version: 1,
metric: true,
right_handed: true,
z_up: true,
},
calibration_id: CalibrationId("cal:golden".into()),
floor_plane: Some(FloorPlane {
normal: [0.0, 0.0, 1.0],
offset_m: 0.0,
}),
model: ModelRef {
id: "pose:golden".into(),
artifact_hash: [3; 32],
},
source: SourceProvenance {
sensor_id: "sensor:golden".into(),
authenticated: false,
replay_protected: false,
},
trust_state: PoseTrustState::Known,
dimensionality: PoseDimensionality::Metric3d,
uncertainty_calibrated: true,
joints,
observer_confidence: Probability::new(0.78).expect("fixture confidence is valid"),
canonical_hash: [0; 32],
};
raw.seal();
raw
}
fn encode_hex(bytes: [u8; 32]) -> String {
let mut encoded = String::with_capacity(64);
for byte in bytes {
write!(&mut encoded, "{byte:02x}").expect("writing to String cannot fail");
}
encoded
}

View File

@@ -0,0 +1 @@
/target/

View File

@@ -0,0 +1,884 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_system_properties"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
dependencies = [
"libc",
]
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
[[package]]
name = "blake3"
version = "1.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ae7bad254120e9e4c63bafc385310756f90c484eac0e36b8317cf09cb92a77"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"cpufeatures",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytemuck"
version = "1.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
[[package]]
name = "cc"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "glam"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da"
[[package]]
name = "glam"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3abb554f8ee44336b72d522e0a7fe86a29e09f839a36022fa869a7dfe941a54b"
[[package]]
name = "glam"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16"
[[package]]
name = "glam"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776"
[[package]]
name = "glam"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525a3e490ba77b8e326fb67d4b44b4bd2f920f44d4cc73ccec50adc68e3bee34"
[[package]]
name = "glam"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8509e6791516e81c1a630d0bd7fbac36d2fa8712a9da8662e716b52d5051ca"
[[package]]
name = "glam"
version = "0.20.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f"
[[package]]
name = "glam"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815"
[[package]]
name = "glam"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774"
[[package]]
name = "glam"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c"
[[package]]
name = "glam"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945"
[[package]]
name = "glam"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
[[package]]
name = "glam"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
[[package]]
name = "glam"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94"
[[package]]
name = "glam"
version = "0.29.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee"
[[package]]
name = "glam"
version = "0.30.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
[[package]]
name = "glam"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7"
[[package]]
name = "glam"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jobserver"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libfuzzer-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "matrixmultiply"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "nalgebra"
version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df76ea0ff5c7e6b88689085804d6132ded0ddb9de5ca5b8aeb9eeadc0508a70a"
dependencies = [
"approx",
"glam 0.14.0",
"glam 0.15.2",
"glam 0.16.0",
"glam 0.17.3",
"glam 0.18.0",
"glam 0.19.0",
"glam 0.20.5",
"glam 0.21.3",
"glam 0.22.0",
"glam 0.23.0",
"glam 0.24.2",
"glam 0.25.0",
"glam 0.27.0",
"glam 0.28.0",
"glam 0.29.3",
"glam 0.30.10",
"glam 0.31.1",
"glam 0.32.1",
"matrixmultiply",
"nalgebra-macros",
"num-complex",
"num-rational",
"num-traits",
"simba",
"typenum",
]
[[package]]
name = "nalgebra-macros"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "ndarray"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
dependencies = [
"matrixmultiply",
"num-complex",
"num-integer",
"num-traits",
"portable-atomic",
"portable-atomic-util",
"rawpointer",
"serde",
]
[[package]]
name = "num-bigint"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "safe_arch"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
dependencies = [
"bytemuck",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "simba"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
"wide",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "uuid"
version = "1.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
dependencies = [
"getrandom",
"js-sys",
"serde_core",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.119",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wide"
version = "0.7.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "wifi-densepose-core"
version = "0.3.2"
dependencies = [
"blake3",
"chrono",
"ndarray",
"num-complex",
"num-traits",
"serde",
"thiserror",
"uuid",
]
[[package]]
name = "wifi-densepose-physics"
version = "0.1.0"
dependencies = [
"bitflags",
"blake3",
"nalgebra",
"serde",
"smallvec",
"thiserror",
"tracing",
"wifi-densepose-core",
]
[[package]]
name = "wifi-densepose-physics-fuzz"
version = "0.0.0"
dependencies = [
"libfuzzer-sys",
"serde_json",
"wifi-densepose-core",
"wifi-densepose-physics",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"

View File

@@ -0,0 +1,24 @@
[package]
name = "wifi-densepose-physics-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
serde_json = "1"
wifi-densepose-core = { path = "../../wifi-densepose-core", features = ["serde"] }
wifi-densepose-physics = { path = ".." }
[workspace]
members = ["."]
[[bin]]
name = "pose_frame"
path = "fuzz_targets/pose_frame.rs"
test = false
doc = false
bench = false

View File

@@ -0,0 +1,15 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
fuzz_target!(|bytes: &[u8]| {
if bytes.len() > 64 * 1024 {
return;
}
if let Ok(raw) = serde_json::from_slice::<wifi_densepose_core::PoseObservationV2>(bytes) {
if let Ok(mut engine) = PhysicsEngine::new(PhysicsConfig::default()) {
let _ = engine.process(&raw, raw.timestamp_ns);
}
}
});

View File

@@ -0,0 +1,164 @@
//! Bounded runtime configuration.
use wifi_densepose_core::PhysicsMode;
/// Runtime limits. Untrusted frames cannot modify these values.
#[derive(Clone, Debug)]
pub struct PhysicsConfig {
pub mode: PhysicsMode,
pub max_tracks: usize,
pub deadline_ms: u64,
pub max_iterations: u8,
pub max_joint_correction_m: f32,
pub max_root_correction_m: f32,
pub max_frame_gap_ms: u64,
pub track_reset_gap_ms: u64,
pub stale_after_ms: u64,
pub minimum_joint_confidence: f32,
pub minimum_known_joints: usize,
pub calibration_joint_confidence: f32,
pub solver_epsilon_m: f32,
pub max_velocity_mps: f32,
pub max_acceleration_mps2: f32,
pub room_bound_m: f32,
pub image_coordinate_bound: f32,
pub beta_residual: f32,
pub beta_intervention: f32,
pub dynamics_audit: bool,
pub dynamics_substeps: u8,
pub dynamics_dt_seconds: f32,
}
impl Default for PhysicsConfig {
fn default() -> Self {
Self {
mode: PhysicsMode::Audit,
max_tracks: 4,
deadline_ms: 5,
max_iterations: 4,
max_joint_correction_m: 0.20,
max_root_correction_m: 0.10,
max_frame_gap_ms: 250,
track_reset_gap_ms: 500,
stale_after_ms: 500,
minimum_joint_confidence: 0.10,
minimum_known_joints: 10,
calibration_joint_confidence: 0.70,
solver_epsilon_m: 0.001,
max_velocity_mps: 8.0,
max_acceleration_mps2: 40.0,
room_bound_m: 100.0,
image_coordinate_bound: 16_384.0,
beta_residual: 1.0,
beta_intervention: 2.0,
dynamics_audit: false,
dynamics_substeps: 2,
dynamics_dt_seconds: 1.0 / 30.0,
}
}
}
impl PhysicsConfig {
/// Validate operator-controlled limits before activation.
pub fn validate(&self) -> Result<(), &'static str> {
if self.max_tracks == 0 || self.max_tracks > 256 {
return Err("max_tracks must be in 1..=256");
}
if self.max_iterations == 0 || self.max_iterations > 8 {
return Err("max_iterations must be in 1..=8");
}
if self.deadline_ms == 0 || self.deadline_ms > 1000 {
return Err("deadline_ms must be in 1..=1000");
}
let finite_positive = [
self.max_joint_correction_m,
self.max_root_correction_m,
self.solver_epsilon_m,
self.max_velocity_mps,
self.max_acceleration_mps2,
self.room_bound_m,
self.image_coordinate_bound,
self.beta_residual,
self.beta_intervention,
self.dynamics_dt_seconds,
];
if finite_positive.iter().any(|v| !v.is_finite() || *v <= 0.0) {
return Err("numeric limits must be finite and positive");
}
if self.image_coordinate_bound > 1_000_000.0 {
return Err("image coordinate bound is excessive");
}
if !(0.0..=1.0).contains(&self.minimum_joint_confidence)
|| !(0.0..=1.0).contains(&self.calibration_joint_confidence)
{
return Err("confidence limits must be in [0, 1]");
}
if !(1..=17).contains(&self.minimum_known_joints) {
return Err("minimum_known_joints must be in 1..=17");
}
if self.max_frame_gap_ms == 0
|| self.track_reset_gap_ms < self.max_frame_gap_ms
|| self.stale_after_ms < self.max_frame_gap_ms
{
return Err("frame/reset/stale gaps must be ordered and non-zero");
}
if self.max_root_correction_m > self.max_joint_correction_m
|| self.solver_epsilon_m >= self.max_joint_correction_m
{
return Err("correction limits are inconsistent");
}
if self.calibration_joint_confidence < self.minimum_joint_confidence {
return Err("calibration confidence must meet the joint-use threshold");
}
if self.dynamics_audit && !cfg!(feature = "dynamics") {
return Err("dynamics_audit requires the dynamics Cargo feature");
}
if !(1..=8).contains(&self.dynamics_substeps)
|| !self.dynamics_dt_seconds.is_finite()
|| !(1.0 / 240.0..=0.05).contains(&self.dynamics_dt_seconds)
{
return Err("dynamics limits are invalid");
}
Ok(())
}
/// Stable hash used in idempotency and provenance.
#[must_use]
pub fn canonical_hash(&self) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(b"ruview.pose-physics-config-v1\0");
h.update(&[
self.mode as u8,
self.max_iterations,
self.dynamics_audit.into(),
self.dynamics_substeps,
]);
for value in [
self.max_tracks as u64,
self.deadline_ms,
self.max_frame_gap_ms,
self.track_reset_gap_ms,
self.stale_after_ms,
self.minimum_known_joints as u64,
] {
h.update(&value.to_le_bytes());
}
for value in [
self.max_joint_correction_m,
self.max_root_correction_m,
self.minimum_joint_confidence,
self.calibration_joint_confidence,
self.solver_epsilon_m,
self.max_velocity_mps,
self.max_acceleration_mps2,
self.room_bound_m,
self.image_coordinate_bound,
self.beta_residual,
self.beta_intervention,
self.dynamics_dt_seconds,
] {
h.update(&value.to_bits().to_le_bytes());
}
*h.finalize().as_bytes()
}
}

View File

@@ -0,0 +1,26 @@
//! Bone-length consistency.
use crate::skeleton::{
distance, virtual_joints, SkeletonPosterior, CONSTRAINED_EDGE_COUNT, OBSERVED_EDGES,
VIRTUAL_NECK_EDGE, VIRTUAL_TRUNK_EDGE,
};
/// Mean absolute edge-length residual.
#[must_use]
pub fn bone_residual(joints: &[[f32; 3]; 17], posterior: &SkeletonPosterior) -> f32 {
let observed_total = OBSERVED_EDGES
.iter()
.enumerate()
.map(|(index, (a, b))| {
let current = distance(joints[*a], joints[*b]);
(current - posterior.target(index, current)).abs()
})
.sum::<f32>();
let (pelvis, thorax) = virtual_joints(joints);
let trunk = distance(pelvis, thorax);
let neck = distance(thorax, joints[0]);
let total = observed_total
+ (trunk - posterior.target(VIRTUAL_TRUNK_EDGE, trunk)).abs()
+ (neck - posterior.target(VIRTUAL_NECK_EDGE, neck)).abs();
total / CONSTRAINED_EDGE_COUNT as f32
}

View File

@@ -0,0 +1,32 @@
//! Foot-contact hypotheses derived from floor proximity and tangential motion.
use nalgebra::Vector3;
use wifi_densepose_core::FloorPlane;
use super::TemporalHistory;
/// Maximum tangential foot displacement while an ankle is close enough to the floor
/// to be a contact hypothesis. This is never labeled as measured contact.
#[must_use]
pub fn contact_slide_residual(
joints: &[[f32; 3]; 17],
floor: FloorPlane,
history: &TemporalHistory,
timestamp_ns: u64,
) -> f32 {
let Some((previous, previous_ns)) = &history.previous else {
return 0.0;
};
if timestamp_ns <= *previous_ns {
return 0.0;
}
let normal = Vector3::from(floor.normal);
[15usize, 16usize]
.into_iter()
.filter(|&index| floor.signed_distance(joints[index]).abs() <= 0.05)
.map(|index| {
let displacement = Vector3::from(joints[index]) - Vector3::from(previous[index]);
(displacement - normal * displacement.dot(&normal)).norm()
})
.fold(0.0, f32::max)
}

View File

@@ -0,0 +1,25 @@
//! Calibrated floor-plane constraint. It prevents penetration only.
use wifi_densepose_core::FloorPlane;
/// Maximum penetration among observed joints.
#[must_use]
pub fn floor_penetration(joints: &[[f32; 3]; 17], floor: FloorPlane) -> f32 {
joints
.iter()
.map(|point| (-floor.signed_distance(*point)).max(0.0))
.fold(0.0, f32::max)
}
/// Move penetrating joints to the plane. No upright or contact prior is used.
pub fn project_floor(joints: &mut [[f32; 3]; 17], floor: FloorPlane, _weights: &[f32; 17]) {
for joint in joints.iter_mut() {
let penetration = (-floor.signed_distance(*joint)).max(0.0);
if penetration <= 0.0 {
continue;
}
for (coordinate, normal) in joint.iter_mut().zip(floor.normal) {
*coordinate += normal * penetration;
}
}
}

View File

@@ -0,0 +1,76 @@
//! Broad hinge-angle constraints that reject only anatomically impossible poses.
use nalgebra::Vector3;
/// `(parent, hinge, distal, minimum angle, maximum angle)`.
const HINGES: [(usize, usize, usize, f32, f32); 4] = [
(5, 7, 9, 0.087_266_46, 3.124_139_3),
(6, 8, 10, 0.087_266_46, 3.124_139_3),
(11, 13, 15, 0.087_266_46, 3.124_139_3),
(12, 14, 16, 0.087_266_46, 3.124_139_3),
];
/// Maximum angular violation in radians.
#[must_use]
pub fn joint_limit_residual(joints: &[[f32; 3]; 17]) -> f32 {
HINGES
.iter()
.map(|&(parent, hinge, distal, minimum, maximum)| {
let angle = angle_at(joints[parent], joints[hinge], joints[distal]);
if angle < minimum {
minimum - angle
} else if angle > maximum {
angle - maximum
} else {
0.0
}
})
.fold(0.0, f32::max)
}
/// Move only the distal point toward a legal hinge angle. The movement weight
/// makes uncertain joints converge faster while the outer correction cap
/// remains authoritative.
pub fn project_joint_limits(joints: &mut [[f32; 3]; 17], weights: &[f32; 17]) {
for &(parent, hinge, distal, minimum, maximum) in &HINGES {
let parent_direction = Vector3::from(joints[parent]) - Vector3::from(joints[hinge]);
let distal_vector = Vector3::from(joints[distal]) - Vector3::from(joints[hinge]);
let parent_length = parent_direction.norm();
let distal_length = distal_vector.norm();
if parent_length <= 1.0e-6 || distal_length <= 1.0e-6 {
continue;
}
let parent_unit = parent_direction / parent_length;
let distal_unit = distal_vector / distal_length;
let angle = parent_unit.dot(&distal_unit).clamp(-1.0, 1.0).acos();
let target = angle.clamp(minimum, maximum);
if (target - angle).abs() <= f32::EPSILON {
continue;
}
let mut perpendicular = distal_unit - parent_unit * parent_unit.dot(&distal_unit);
if perpendicular.norm_squared() <= 1.0e-8 {
let basis = if parent_unit.x.abs() < 0.8 {
Vector3::x()
} else {
Vector3::y()
};
perpendicular = parent_unit.cross(&basis);
}
let perpendicular = perpendicular.normalize();
let target_direction = parent_unit * target.cos() + perpendicular * target.sin();
let target_point = Vector3::from(joints[hinge]) + target_direction * distal_length;
let alpha = weights[distal].clamp(0.05, 1.0);
joints[distal] =
(Vector3::from(joints[distal]) * (1.0 - alpha) + target_point * alpha).into();
}
}
fn angle_at(parent: [f32; 3], hinge: [f32; 3], distal: [f32; 3]) -> f32 {
let a = Vector3::from(parent) - Vector3::from(hinge);
let b = Vector3::from(distal) - Vector3::from(hinge);
let denominator = a.norm() * b.norm();
if denominator <= 1.0e-8 {
return 0.0;
}
(a.dot(&b) / denominator).clamp(-1.0, 1.0).acos()
}

View File

@@ -0,0 +1,58 @@
//! Deterministic kinematic residuals.
mod bone;
mod contact;
mod floor;
mod joint_limit;
mod temporal;
pub use bone::bone_residual;
pub use contact::contact_slide_residual;
pub use floor::{floor_penetration, project_floor};
pub use joint_limit::{joint_limit_residual, project_joint_limits};
pub use temporal::{temporal_residuals, TemporalHistory};
use crate::skeleton::{distance, virtual_joints};
use wifi_densepose_core::ConstraintResiduals;
/// Evaluate residuals without mutating the candidate.
#[must_use]
pub fn audit(
joints: &[[f32; 3]; 17],
posterior: &crate::skeleton::SkeletonPosterior,
floor: wifi_densepose_core::FloorPlane,
history: &TemporalHistory,
timestamp_ns: u64,
) -> ConstraintResiduals {
let bone_m = bone_residual(joints, posterior);
let floor_penetration_m = floor_penetration(joints, floor);
let (velocity_mps, acceleration_mps2, temporal_jerk) =
temporal_residuals(joints, history, timestamp_ns);
let contact_m = contact_slide_residual(joints, floor, history, timestamp_ns);
let (pelvis, thorax) = virtual_joints(joints);
let trunk = distance(pelvis, thorax);
let trunk_violation = if (0.05..=1.2).contains(&trunk) {
0.0
} else {
1.0
};
let joint_limit_rad = joint_limit_residual(joints).max(trunk_violation);
let normalized_total = (bone_m / 0.05
+ floor_penetration_m / 0.05
+ velocity_mps / 8.0
+ acceleration_mps2 / 40.0
+ joint_limit_rad
+ contact_m / 0.25)
/ 6.0;
ConstraintResiduals {
bone_m,
joint_limit_rad,
velocity_mps,
acceleration_mps2,
temporal_jerk,
floor_penetration_m,
contact_m,
collision_m: 0.0,
normalized_total,
}
}

View File

@@ -0,0 +1,65 @@
//! Bounded temporal state and residuals.
use nalgebra::Vector3;
use crate::skeleton::distance;
/// At most two previous frames, held only for a local track.
#[derive(Clone, Debug, Default)]
pub struct TemporalHistory {
pub previous: Option<([[f32; 3]; 17], u64)>,
pub previous_previous: Option<([[f32; 3]; 17], u64)>,
}
impl TemporalHistory {
/// Commit a validated observation/candidate.
pub fn push(&mut self, joints: [[f32; 3]; 17], timestamp_ns: u64) {
self.previous_previous = self.previous.take();
self.previous = Some((joints, timestamp_ns));
}
/// Drop derivatives after a discontinuity.
pub fn reset(&mut self) {
*self = Self::default();
}
}
/// Maximum velocity, acceleration, and normalized jerk.
#[must_use]
pub fn temporal_residuals(
joints: &[[f32; 3]; 17],
history: &TemporalHistory,
timestamp_ns: u64,
) -> (f32, f32, f32) {
let Some((previous, previous_ns)) = &history.previous else {
return (0.0, 0.0, 0.0);
};
let dt = seconds(timestamp_ns.saturating_sub(*previous_ns));
if dt <= 0.0 {
return (f32::INFINITY, f32::INFINITY, f32::INFINITY);
}
let velocity = joints
.iter()
.zip(previous)
.map(|(a, b)| distance(*a, *b) / dt)
.fold(0.0, f32::max);
let Some((older, older_ns)) = &history.previous_previous else {
return (velocity, 0.0, 0.0);
};
let previous_dt = seconds(previous_ns.saturating_sub(*older_ns));
if previous_dt <= 0.0 {
return (velocity, f32::INFINITY, f32::INFINITY);
}
let mut acceleration = 0.0f32;
for index in 0..17 {
let v_now = (Vector3::from(joints[index]) - Vector3::from(previous[index])) / dt;
let v_previous =
(Vector3::from(previous[index]) - Vector3::from(older[index])) / previous_dt;
acceleration = acceleration.max((v_now - v_previous).norm() / dt);
}
(velocity, acceleration, acceleration * dt)
}
fn seconds(nanoseconds: u64) -> f32 {
nanoseconds as f32 / 1_000_000_000.0
}

View File

@@ -0,0 +1,236 @@
//! Optional bounded Rapier articulated-dynamics audit.
//!
//! All bodies, constraints, and snapshots are process-owned. The auditor
//! scores a candidate but never mutates or selects the public pose.
use rapier3d::prelude::{
ColliderBuilder, PhysicsWorld, Pose, RigidBodyBuilder, RigidBodyHandle, Rotation,
SphericalJointBuilder, Vector,
};
use wifi_densepose_core::FloorPlane;
use crate::skeleton::OBSERVED_EDGES;
const SEGMENT_RADIUS_M: f32 = 0.035;
const MAX_SUBSTEPS: u8 = 8;
#[derive(Clone, Debug)]
struct Segment {
handle: RigidBodyHandle,
target_position: Pose,
endpoints: (usize, usize),
midpoint: Vector,
rotation: Rotation,
}
#[derive(Clone, Debug)]
struct JointAnchor {
first: usize,
second: usize,
local_first: Vector,
local_second: Vector,
}
/// Scalar dynamics assessment safe for metrics and provenance.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct DynamicsAssessment {
pub stable: bool,
pub segment_count: u8,
pub joint_count: u8,
pub contact_count: u16,
pub substeps: u8,
pub max_tracking_error_m: f32,
pub max_joint_anchor_error_m: f32,
pub max_floor_penetration_m: f32,
pub max_control_effort: f32,
}
/// Process-owned humanoid assembled from capsule segments and spherical joints.
pub struct DynamicsAuditor {
world: PhysicsWorld,
segments: Vec<Segment>,
anchors: Vec<JointAnchor>,
floor: FloorPlane,
}
impl DynamicsAuditor {
/// Build a bounded articulated model from one candidate pose.
#[must_use]
pub fn new(joints: &[[f32; 3]; 17], floor: FloorPlane) -> Self {
let mut world = PhysicsWorld::new();
world.gravity = Vector::new(0.0, 0.0, -9.81);
let floor_normal = Vector::from_array(floor.normal);
let floor_point = floor_normal * -floor.offset_m;
let floor_rotation = Rotation::from_rotation_arc(Vector::Z, floor_normal);
world.insert(
RigidBodyBuilder::fixed().pose(Pose::from_parts(floor_point, floor_rotation)),
ColliderBuilder::cuboid(10.0, 10.0, 0.02).friction(0.8),
);
let mut segments = Vec::with_capacity(OBSERVED_EDGES.len());
for endpoints in OBSERVED_EDGES {
let a = Vector::from_array(joints[endpoints.0]);
let b = Vector::from_array(joints[endpoints.1]);
let delta = b - a;
let length = delta.length().max(SEGMENT_RADIUS_M * 2.0);
let midpoint = (a + b) * 0.5;
let direction = delta.try_normalize().unwrap_or(Vector::Y);
let rotation = Rotation::from_rotation_arc(Vector::Y, direction);
let position = Pose::from_parts(midpoint, rotation);
let (handle, _) = world.insert(
RigidBodyBuilder::dynamic()
.pose(position)
.linear_damping(0.8)
.angular_damping(0.8)
.can_sleep(false),
ColliderBuilder::capsule_y(
(length * 0.5 - SEGMENT_RADIUS_M).max(0.001),
SEGMENT_RADIUS_M,
)
.density(450.0)
.friction(0.7),
);
segments.push(Segment {
handle,
target_position: position,
endpoints,
midpoint,
rotation,
});
}
let mut anchors = Vec::new();
for first in 0..segments.len() {
for second in (first + 1)..segments.len() {
let Some(shared) =
shared_endpoint(segments[first].endpoints, segments[second].endpoints)
else {
continue;
};
let point = Vector::from_array(joints[shared]);
let local_first =
segments[first].rotation.inverse() * (point - segments[first].midpoint);
let local_second =
segments[second].rotation.inverse() * (point - segments[second].midpoint);
world.insert_impulse_joint(
segments[first].handle,
segments[second].handle,
SphericalJointBuilder::new()
.local_anchor1(local_first)
.local_anchor2(local_second)
.contacts_enabled(false),
);
anchors.push(JointAnchor {
first,
second,
local_first,
local_second,
});
}
}
Self {
world,
segments,
anchors,
floor,
}
}
/// Run bounded PD-controlled audit steps and return scalar residuals.
#[must_use]
pub fn audit(
&mut self,
joints: &[[f32; 3]; 17],
dt_seconds: f32,
requested_substeps: u8,
) -> DynamicsAssessment {
let substeps = requested_substeps.clamp(1, MAX_SUBSTEPS);
self.world.integration_parameters.dt =
dt_seconds.clamp(1.0 / 240.0, 0.05) / f32::from(substeps);
let mut max_control_effort = 0.0f32;
for segment in &mut self.segments {
let a = Vector::from_array(joints[segment.endpoints.0]);
let b = Vector::from_array(joints[segment.endpoints.1]);
let midpoint = (a + b) * 0.5;
let direction = (b - a).try_normalize().unwrap_or(Vector::Y);
let rotation = Rotation::from_rotation_arc(Vector::Y, direction);
segment.target_position = Pose::from_parts(midpoint, rotation);
}
for _ in 0..substeps {
for segment in &self.segments {
let Some(body) = self.world.bodies.get_mut(segment.handle) else {
continue;
};
let position_error = segment.target_position.translation - body.translation();
let rotation_error =
(segment.target_position.rotation * body.rotation().inverse()).to_scaled_axis();
let force = position_error * 120.0 - body.linvel() * 18.0;
let torque = rotation_error * 35.0 - body.angvel() * 8.0;
max_control_effort = max_control_effort.max(force.length()).max(torque.length());
body.add_force(force, true);
body.add_torque(torque, true);
}
self.world.step();
}
let max_tracking_error_m = self
.segments
.iter()
.filter_map(|segment| {
self.world
.bodies
.get(segment.handle)
.map(|body| (segment, body))
})
.map(|(segment, body)| {
(segment.target_position.translation - body.translation()).length()
})
.fold(0.0, f32::max);
let max_joint_anchor_error_m = self
.anchors
.iter()
.filter_map(|anchor| {
let first = self.world.bodies.get(self.segments[anchor.first].handle)?;
let second = self.world.bodies.get(self.segments[anchor.second].handle)?;
let point_first = first.position() * anchor.local_first;
let point_second = second.position() * anchor.local_second;
Some((point_first - point_second).length())
})
.fold(0.0, f32::max);
let max_floor_penetration_m = self
.segments
.iter()
.filter_map(|segment| self.world.bodies.get(segment.handle))
.map(|body| (-self.floor.signed_distance(body.translation().to_array())).max(0.0))
.fold(0.0, f32::max);
let contact_count = self
.world
.narrow_phase
.contact_pairs()
.filter(|pair| pair.has_any_active_contact())
.count()
.try_into()
.unwrap_or(u16::MAX);
let stable = max_tracking_error_m.is_finite()
&& max_joint_anchor_error_m.is_finite()
&& max_floor_penetration_m.is_finite()
&& max_control_effort.is_finite();
DynamicsAssessment {
stable,
segment_count: self.segments.len().try_into().unwrap_or(u8::MAX),
joint_count: self.anchors.len().try_into().unwrap_or(u8::MAX),
contact_count,
substeps,
max_tracking_error_m,
max_joint_anchor_error_m,
max_floor_penetration_m,
max_control_effort,
}
}
}
fn shared_endpoint(first: (usize, usize), second: (usize, usize)) -> Option<usize> {
[first.0, first.1]
.into_iter()
.find(|endpoint| *endpoint == second.0 || *endpoint == second.1)
}

View File

@@ -0,0 +1,779 @@
//! Stateful, bounded top-level control flow.
use std::{collections::HashMap, time::Instant};
use wifi_densepose_core::{
AbstentionReason, ConstraintResiduals, ContactHypothesis, ContactState, PhysicsMode,
PhysicsProvenance, PoseDimensionality, PoseObservationV2, PoseRefinementV1, PoseTrustState,
Probability, RefinementDisposition, POSE_OBSERVATION_SCHEMA_VERSION,
};
use crate::{
config::PhysicsConfig,
constraints::{self, TemporalHistory},
error::PhysicsError,
projector,
skeleton::SkeletonPosterior,
uncertainty::movement_weight,
};
#[derive(Default)]
struct TrackState {
last_sequence: Option<u64>,
last_timestamp_ns: Option<u64>,
last_hash: Option<[u8; 32]>,
last_config_hash: Option<[u8; 32]>,
cached: Option<PoseRefinementV1>,
skeleton: SkeletonPosterior,
temporal: TemporalHistory,
last_calibration_id: Option<String>,
#[cfg(feature = "dynamics")]
dynamics: Option<crate::dynamics::DynamicsAuditor>,
}
/// Evidence receipt produced only after the serving boundary verifies the
/// signed configuration, release gate, sensor authentication, and replay
/// policy. The physics crate binds it to one exact configuration hash.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CorrectionAuthorization {
config_hash: [u8; 32],
evidence_hash: [u8; 32],
}
impl CorrectionAuthorization {
/// Construct a receipt from already verified, hash-addressed evidence.
pub fn from_verified_evidence(
config_hash: [u8; 32],
evidence_hash: [u8; 32],
) -> Result<Self, PhysicsError> {
if config_hash == [0; 32] || evidence_hash == [0; 32] {
return Err(PhysicsError::InvalidCorrectionAuthorization(
"configuration and evidence hashes must be non-zero",
));
}
Ok(Self {
config_hash,
evidence_hash,
})
}
}
/// Per-frame receipt returned by the ADR-305 verifier after signature,
/// freshness, sequence, replay-window, calibration, and trust-state checks.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VerifiedFrameContext {
raw_hash: [u8; 32],
sensor_epoch: u64,
sequence: u64,
sensor_id: String,
calibration_id: String,
receipt_hash: [u8; 32],
}
impl VerifiedFrameContext {
/// Bind process-owned verification evidence to one canonical frame.
pub fn from_verified_envelope(
raw_hash: [u8; 32],
sensor_epoch: u64,
sequence: u64,
sensor_id: String,
calibration_id: String,
receipt_hash: [u8; 32],
) -> Result<Self, PhysicsError> {
if raw_hash == [0; 32]
|| receipt_hash == [0; 32]
|| sensor_id.is_empty()
|| calibration_id.is_empty()
{
return Err(PhysicsError::InvalidCorrectionAuthorization(
"verified frame receipt is incomplete",
));
}
Ok(Self {
raw_hash,
sensor_epoch,
sequence,
sensor_id,
calibration_id,
receipt_hash,
})
}
fn matches(&self, raw: &PoseObservationV2) -> bool {
self.raw_hash == raw.canonical_hash
&& self.sensor_epoch == raw.sensor_epoch
&& self.sequence == raw.sequence
&& self.sensor_id == raw.source.sensor_id
&& self.calibration_id == raw.calibration_id.0
&& self.receipt_hash != [0; 32]
}
}
/// In-memory physics engine. Track state is anonymous and discarded on expiry.
pub struct PhysicsEngine {
config: PhysicsConfig,
config_hash: [u8; 32],
tracks: HashMap<(wifi_densepose_core::TrackId, u64), TrackState>,
correction_authorization: Option<CorrectionAuthorization>,
}
impl PhysicsEngine {
/// Activate a validated trusted configuration.
pub fn new(config: PhysicsConfig) -> Result<Self, PhysicsError> {
config
.validate()
.map_err(PhysicsError::InvalidConfiguration)?;
let config_hash = config.canonical_hash();
Ok(Self {
config,
config_hash,
tracks: HashMap::new(),
correction_authorization: None,
})
}
/// Current immutable configuration.
#[must_use]
pub const fn config(&self) -> &PhysicsConfig {
&self.config
}
/// Hash of the exact active configuration.
#[must_use]
pub const fn config_hash(&self) -> [u8; 32] {
self.config_hash
}
/// Hash a prospective mode without activating it, so a signed evidence
/// receipt can be bound before the atomic transition.
#[must_use]
pub fn config_hash_for_mode(&self, mode: PhysicsMode) -> [u8; 32] {
let mut prospective = self.config.clone();
prospective.mode = mode;
prospective.canonical_hash()
}
/// Activate a correction receipt after the serving boundary has verified it.
pub fn authorize_correction(
&mut self,
authorization: CorrectionAuthorization,
) -> Result<(), PhysicsError> {
self.correction_authorization = Some(authorization);
Ok(())
}
/// Authenticated control planes may change rollout mode without restart.
pub fn set_mode(&mut self, mode: PhysicsMode) -> Result<(), PhysicsError> {
let mut prospective = self.config.clone();
prospective.mode = mode;
let prospective_hash = prospective.canonical_hash();
if mode.selects_correction()
&& self
.correction_authorization
.is_none_or(|receipt| receipt.config_hash != prospective_hash)
{
return Err(PhysicsError::InvalidCorrectionAuthorization(
"correction mode requires evidence bound to the target configuration",
));
}
self.config.mode = mode;
self.config_hash = prospective_hash;
if matches!(mode, PhysicsMode::Off) {
self.tracks.clear();
}
Ok(())
}
/// Process one frame and always return a typed assessment.
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn process(&mut self, raw: &PoseObservationV2, now_ns: u64) -> PoseRefinementV1 {
self.process_with_context(raw, now_ns, None)
}
/// Process a frame with process-owned source verification evidence.
#[must_use]
pub fn process_verified(
&mut self,
raw: &PoseObservationV2,
now_ns: u64,
verification: &VerifiedFrameContext,
) -> PoseRefinementV1 {
self.process_with_context(raw, now_ns, Some(verification))
}
#[allow(clippy::too_many_lines)]
fn process_with_context(
&mut self,
raw: &PoseObservationV2,
now_ns: u64,
verification: Option<&VerifiedFrameContext>,
) -> PoseRefinementV1 {
let started = Instant::now();
if let Some(reason) = self.validate_contract(raw, now_ns) {
return self.raw_result(raw, RefinementDisposition::Rejected, Some(reason));
}
if self.config.mode == PhysicsMode::Off {
return self.raw_result(
raw,
RefinementDisposition::Bypassed,
Some(AbstentionReason::ModeOff),
);
}
if raw.trust_state == PoseTrustState::Unknown {
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::OodUnknown),
);
}
if raw.dimensionality == PoseDimensionality::Image2d {
return self.audit_2d(raw, started);
}
if self.config.mode.selects_correction()
&& self
.correction_authorization
.is_none_or(|receipt| receipt.config_hash != self.config_hash)
{
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::CorrectionNotAuthorized),
);
}
if self.config.mode.selects_correction()
&& verification.is_none_or(|receipt| !receipt.matches(raw))
{
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::SourceUnauthenticated),
);
}
if !raw.uncertainty_calibrated {
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::UncertaintyUncalibrated),
);
}
let Some(floor) = raw.floor_plane else {
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::CalibrationUnavailable),
);
};
if !floor.is_valid() {
return self.raw_result(
raw,
RefinementDisposition::Rejected,
Some(AbstentionReason::CoordinateFrameMismatch),
);
}
self.expire_tracks(now_ns);
let key = (raw.track_id.clone(), raw.sensor_epoch);
if !self.tracks.contains_key(&key) && self.tracks.len() >= self.config.max_tracks {
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::TrackCapacity),
);
}
let mut state = self.tracks.remove(&key).unwrap_or_default();
if state.last_sequence == Some(raw.sequence) {
if state.last_hash == Some(raw.canonical_hash)
&& state.last_config_hash == Some(self.config_hash)
{
if let Some(cached) = state.cached.clone() {
self.tracks.insert(key, state);
return cached;
}
}
self.tracks.insert(key, state);
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::ReplayRejected),
);
}
if state
.last_sequence
.is_some_and(|sequence| raw.sequence < sequence)
|| state
.last_timestamp_ns
.is_some_and(|timestamp| raw.timestamp_ns <= timestamp)
{
state.temporal.reset();
self.tracks.insert(key, state);
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::NonMonotonicInput),
);
}
if state.last_timestamp_ns.is_some_and(|timestamp| {
raw.timestamp_ns.saturating_sub(timestamp) > self.config.track_reset_gap_ms * 1_000_000
}) {
state = TrackState::default();
} else if state.last_timestamp_ns.is_some_and(|timestamp| {
raw.timestamp_ns.saturating_sub(timestamp) > self.config.max_frame_gap_ms * 1_000_000
}) {
state.temporal.reset();
}
if state
.last_calibration_id
.as_ref()
.is_some_and(|calibration| calibration != &raw.calibration_id.0)
{
state = TrackState::default();
}
let known = raw
.joints
.iter()
.filter(|joint| {
joint.visibility != wifi_densepose_core::JointVisibility::Unknown
&& joint.confidence.get() >= self.config.minimum_joint_confidence
})
.count();
if known < self.config.minimum_known_joints {
self.tracks.insert(key, state);
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::TooFewKnownJoints),
);
}
let observed = raw.joints.map(|joint| joint.position_m);
let confidence = raw.joints.map(|joint| joint.confidence.get());
let weights = raw.joints.map(|joint| movement_weight(joint.covariance_m2));
let audit = constraints::audit(
&observed,
&state.skeleton,
floor,
&state.temporal,
raw.timestamp_ns,
);
state.skeleton.observe(
&observed,
&confidence,
self.config.calibration_joint_confidence,
);
if self.config.mode == PhysicsMode::Audit {
let result = self.build_result(
raw,
RefinementDisposition::Audited,
false,
None,
wifi_densepose_core::InterventionSummary {
elapsed_us: elapsed_us(started),
..wifi_densepose_core::InterventionSummary::default()
},
audit,
None,
None,
);
state.temporal.push(observed, raw.timestamp_ns);
state.last_sequence = Some(raw.sequence);
state.last_timestamp_ns = Some(raw.timestamp_ns);
state.last_hash = Some(raw.canonical_hash);
state.last_config_hash = Some(self.config_hash);
state.last_calibration_id = Some(raw.calibration_id.0.clone());
state.cached = Some(result.clone());
self.tracks.insert(key, state);
return result;
}
let projection = projector::project(
&observed,
&weights,
&state.skeleton,
&state.temporal,
raw.timestamp_ns,
floor,
&self.config,
started,
);
let result = match projection {
Err(reason) => self.raw_result(raw, RefinementDisposition::Abstained, Some(reason)),
Ok(candidate) => {
#[allow(unused_mut)]
let mut refined_residuals = constraints::audit(
&candidate.joints,
&state.skeleton,
floor,
&state.temporal,
raw.timestamp_ns,
);
#[cfg(feature = "dynamics")]
let dynamics = if self.config.dynamics_audit {
let auditor = state.dynamics.get_or_insert_with(|| {
crate::dynamics::DynamicsAuditor::new(&candidate.joints, floor)
});
let assessment = auditor.audit(
&candidate.joints,
self.config.dynamics_dt_seconds,
self.config.dynamics_substeps,
);
refined_residuals.collision_m = assessment.max_floor_penetration_m;
refined_residuals.normalized_total += (assessment.max_tracking_error_m / 0.05
+ assessment.max_joint_anchor_error_m / 0.02
+ assessment.max_floor_penetration_m / 0.05)
/ 3.0;
Some(wifi_densepose_core::DynamicsResiduals {
stable: assessment.stable,
segment_count: assessment.segment_count,
joint_count: assessment.joint_count,
contact_count: assessment.contact_count,
substeps: assessment.substeps,
tracking_error_m: assessment.max_tracking_error_m,
joint_anchor_error_m: assessment.max_joint_anchor_error_m,
floor_penetration_m: assessment.max_floor_penetration_m,
control_effort: assessment.max_control_effort,
})
} else {
None
};
#[cfg(not(feature = "dynamics"))]
let dynamics: Option<wifi_densepose_core::DynamicsResiduals> = None;
if started.elapsed() >= std::time::Duration::from_millis(self.config.deadline_ms) {
self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::DeadlineExceeded),
)
} else {
let (disposition, selected, refined) = match self.config.mode {
PhysicsMode::Audit => unreachable!("audit returned before projection"),
PhysicsMode::ShadowCorrect => (
RefinementDisposition::Shadowed,
false,
Some(candidate.joints),
),
PhysicsMode::OptInCorrect | PhysicsMode::DefaultCorrect => (
RefinementDisposition::Corrected,
true,
Some(candidate.joints),
),
PhysicsMode::Off => unreachable!("off returned before projection"),
};
let mut result = self.build_result(
raw,
disposition,
selected,
refined,
candidate.intervention,
audit,
Some(refined_residuals),
None,
);
result.dynamics = dynamics;
if result.dynamics.is_some() {
result.provenance.engine = "kinematic-pbd+rapier-audit".into();
result.seal();
}
result
}
}
};
let committed_joints = if result.selected {
result.refined_joints_m.unwrap_or(observed)
} else {
observed
};
state.temporal.push(committed_joints, raw.timestamp_ns);
state.last_sequence = Some(raw.sequence);
state.last_timestamp_ns = Some(raw.timestamp_ns);
state.last_hash = Some(raw.canonical_hash);
state.last_config_hash = Some(self.config_hash);
state.last_calibration_id = Some(raw.calibration_id.0.clone());
state.cached = Some(result.clone());
self.tracks.insert(key, state);
debug_assert!(result.effective_confidence.get() <= raw.observer_confidence.get());
debug_assert_eq!(result.raw_observation_hash, raw.canonical_hash);
result
}
fn validate_contract(&self, raw: &PoseObservationV2, now_ns: u64) -> Option<AbstentionReason> {
if raw.schema_version != POSE_OBSERVATION_SCHEMA_VERSION {
return Some(AbstentionReason::UnsupportedSchema);
}
if raw.compute_canonical_hash() != raw.canonical_hash {
return Some(AbstentionReason::HashMismatch);
}
if raw.track_id.0.is_empty()
|| raw.track_id.0.len() > 128
|| raw.frame.name.is_empty()
|| raw.frame.name.len() > 128
|| raw.calibration_id.0.is_empty()
|| raw.calibration_id.0.len() > 128
|| raw.model.id.is_empty()
|| raw.model.id.len() > 128
|| raw.source.sensor_id.is_empty()
|| raw.source.sensor_id.len() > 128
{
return Some(AbstentionReason::InvalidNumber);
}
if now_ns.saturating_sub(raw.timestamp_ns) > self.config.stale_after_ms * 1_000_000
|| raw.timestamp_ns > now_ns.saturating_add(50_000_000)
{
return Some(AbstentionReason::StaleInput);
}
let coordinate_bound = match raw.dimensionality {
PoseDimensionality::Metric3d => self.config.room_bound_m,
PoseDimensionality::Image2d => self.config.image_coordinate_bound,
};
for (index, joint) in raw.joints.iter().enumerate() {
if joint.kind != wifi_densepose_core::Coco17Joint::ALL[index]
|| !joint.position_m.iter().all(|v| v.is_finite())
|| joint.position_m.iter().any(|v| v.abs() > coordinate_bound)
{
return Some(AbstentionReason::InvalidNumber);
}
if !joint.covariance_m2.is_positive_semidefinite() {
return Some(AbstentionReason::InvalidCovariance);
}
}
match raw.dimensionality {
PoseDimensionality::Metric3d
if !(raw.frame.metric && raw.frame.right_handed && raw.frame.z_up) =>
{
Some(AbstentionReason::CoordinateFrameMismatch)
}
PoseDimensionality::Image2d if raw.frame.metric => {
Some(AbstentionReason::CoordinateFrameMismatch)
}
_ => None,
}
}
fn audit_2d(&mut self, raw: &PoseObservationV2, started: Instant) -> PoseRefinementV1 {
self.expire_tracks(raw.timestamp_ns);
let key = (raw.track_id.clone(), raw.sensor_epoch);
if !self.tracks.contains_key(&key) && self.tracks.len() >= self.config.max_tracks {
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::TrackCapacity),
);
}
let mut state = self.tracks.remove(&key).unwrap_or_default();
if state.last_sequence == Some(raw.sequence) {
if state.last_hash == Some(raw.canonical_hash)
&& state.last_config_hash == Some(self.config_hash)
{
if let Some(cached) = state.cached.clone() {
self.tracks.insert(key, state);
return cached;
}
}
self.tracks.insert(key, state);
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::ReplayRejected),
);
}
if state
.last_sequence
.is_some_and(|sequence| raw.sequence < sequence)
|| state
.last_timestamp_ns
.is_some_and(|timestamp| raw.timestamp_ns <= timestamp)
{
state.temporal.reset();
self.tracks.insert(key, state);
return self.raw_result(
raw,
RefinementDisposition::Abstained,
Some(AbstentionReason::NonMonotonicInput),
);
}
if state.last_timestamp_ns.is_some_and(|timestamp| {
raw.timestamp_ns.saturating_sub(timestamp) > self.config.track_reset_gap_ms * 1_000_000
}) {
state = TrackState::default();
} else if state.last_timestamp_ns.is_some_and(|timestamp| {
raw.timestamp_ns.saturating_sub(timestamp) > self.config.max_frame_gap_ms * 1_000_000
}) {
state.temporal.reset();
}
if state
.last_calibration_id
.as_ref()
.is_some_and(|calibration| calibration != &raw.calibration_id.0)
{
state = TrackState::default();
}
let joints = raw.joints.map(|joint| joint.position_m);
let residuals = image_residuals(&joints, &state.temporal, raw.timestamp_ns);
let intervention = wifi_densepose_core::InterventionSummary {
elapsed_us: elapsed_us(started),
..wifi_densepose_core::InterventionSummary::default()
};
let result = self.build_result(
raw,
RefinementDisposition::Audited2d,
false,
None,
intervention,
residuals,
None,
None,
);
state.temporal.push(joints, raw.timestamp_ns);
state.last_sequence = Some(raw.sequence);
state.last_timestamp_ns = Some(raw.timestamp_ns);
state.last_hash = Some(raw.canonical_hash);
state.last_config_hash = Some(self.config_hash);
state.last_calibration_id = Some(raw.calibration_id.0.clone());
state.cached = Some(result.clone());
self.tracks.insert(key, state);
result
}
fn raw_result(
&self,
raw: &PoseObservationV2,
disposition: RefinementDisposition,
reason: Option<AbstentionReason>,
) -> PoseRefinementV1 {
self.build_result(
raw,
disposition,
false,
None,
wifi_densepose_core::InterventionSummary::default(),
ConstraintResiduals::default(),
None,
reason,
)
}
#[allow(clippy::too_many_arguments)]
fn build_result(
&self,
raw: &PoseObservationV2,
disposition: RefinementDisposition,
selected: bool,
refined: Option<[[f32; 3]; 17]>,
intervention: wifi_densepose_core::InterventionSummary,
residuals: ConstraintResiduals,
refined_residuals: Option<ConstraintResiduals>,
reason: Option<AbstentionReason>,
) -> PoseRefinementV1 {
let penalty = self.config.beta_residual.mul_add(
residuals.normalized_total,
self.config.beta_intervention * intervention.max_joint_correction_m
/ self.config.max_joint_correction_m,
);
let physics_value = if reason == Some(AbstentionReason::ModeOff) {
1.0
} else if reason.is_some() {
0.0
} else {
(-penalty.max(0.0)).exp().clamp(0.0, 1.0)
};
let physics_confidence = Probability::new(physics_value).unwrap_or(Probability::ZERO);
let effective_value = raw
.observer_confidence
.get()
.min(raw.observer_confidence.get() * physics_value);
let effective_confidence = Probability::new(effective_value).unwrap_or(Probability::ZERO);
let contact_hypotheses = if raw.dimensionality == PoseDimensionality::Metric3d {
[15usize, 16usize].map(|index| {
let probability = raw.floor_plane.map_or(0.0, |floor| {
(1.0 - floor.signed_distance(raw.joints[index].position_m).abs() / 0.05)
.clamp(0.0, 1.0)
});
ContactHypothesis {
state: ContactState::Hypothesis,
probability: Probability::new(probability).unwrap_or(Probability::ZERO),
}
})
} else {
[ContactHypothesis::default(); 2]
};
let mut result = PoseRefinementV1 {
schema_version: 1,
raw_observation_hash: raw.canonical_hash,
mode: self.config.mode,
disposition,
selected,
refined_joints_m: refined,
physics_confidence,
effective_confidence,
intervention,
residuals,
refined_residuals,
dynamics: None,
contact_hypotheses,
provenance: PhysicsProvenance {
engine: "kinematic-pbd".into(),
engine_version: env!("CARGO_PKG_VERSION").into(),
config_hash: self.config_hash,
rf_model_hash: raw.model.artifact_hash,
calibration_id: raw.calibration_id.0.clone(),
learned_artifact_hash: None,
},
reason,
canonical_hash: [0; 32],
};
result.seal();
result
}
fn expire_tracks(&mut self, now_ns: u64) {
let expiry = self.config.track_reset_gap_ms * 1_000_000;
self.tracks.retain(|_, state| {
state
.last_timestamp_ns
.is_none_or(|timestamp| now_ns.saturating_sub(timestamp) <= expiry)
});
}
}
fn image_residuals(
joints: &[[f32; 3]; 17],
temporal: &crate::constraints::TemporalHistory,
timestamp_ns: u64,
) -> ConstraintResiduals {
let lengths = crate::skeleton::OBSERVED_EDGES
.map(|(a, b)| crate::skeleton::distance(joints[a], joints[b]));
let mean = lengths.iter().sum::<f32>() / lengths.len() as f32;
let ratio_variance = if mean > 1.0e-6 {
lengths
.iter()
.map(|length| ((length - mean) / mean).powi(2))
.sum::<f32>()
/ lengths.len() as f32
} else {
0.0
};
let (velocity, acceleration, jerk) =
crate::constraints::temporal_residuals(joints, temporal, timestamp_ns);
ConstraintResiduals {
normalized_total: ratio_variance.sqrt().min(1.0),
velocity_mps: velocity,
acceleration_mps2: acceleration,
temporal_jerk: jerk,
..ConstraintResiduals::default()
}
}
fn elapsed_us(started: Instant) -> u64 {
#[cfg(feature = "deterministic")]
{
let _ = started;
0
}
#[cfg(not(feature = "deterministic"))]
{
started.elapsed().as_micros().try_into().unwrap_or(u64::MAX)
}
}

View File

@@ -0,0 +1,12 @@
//! Typed internal errors. Public frame failures are mapped to abstention reasons.
/// Construction/configuration errors that cannot be represented as a frame result.
#[derive(Debug, thiserror::Error)]
pub enum PhysicsError {
/// Invalid trusted configuration.
#[error("invalid physics configuration: {0}")]
InvalidConfiguration(&'static str),
/// A correction authorization is empty or bound to another configuration.
#[error("invalid correction authorization: {0}")]
InvalidCorrectionAuthorization(&'static str),
}

View File

@@ -0,0 +1,163 @@
//! Signed learned-artifact identity, compatibility, and bounded output.
use super::{
features::FEATURE_WIDTH,
model::{HIDDEN_WIDTH, HISTORY_FRAMES},
};
pub const LEARNED_ARTIFACT_SCHEMA_VERSION: u16 = 1;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LearnedArtifactManifest {
pub schema_version: u16,
pub history_frames: usize,
pub feature_width: usize,
pub hidden_width: usize,
pub model_id: String,
}
impl LearnedArtifactManifest {
#[must_use]
pub fn adr323(model_id: String) -> Self {
Self {
schema_version: LEARNED_ARTIFACT_SCHEMA_VERSION,
history_frames: HISTORY_FRAMES,
feature_width: FEATURE_WIDTH,
hidden_width: HIDDEN_WIDTH,
model_id,
}
}
fn is_compatible(&self) -> bool {
self.schema_version == LEARNED_ARTIFACT_SCHEMA_VERSION
&& self.history_frames == HISTORY_FRAMES
&& self.feature_width == FEATURE_WIDTH
&& self.hidden_width == HIDDEN_WIDTH
&& !self.model_id.is_empty()
&& self.model_id.len() <= 128
}
}
/// Receipt from the existing signed RVF/Cog activation boundary.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignatureVerification {
pub key_id: String,
pub envelope_hash: [u8; 32],
}
impl SignatureVerification {
#[must_use]
pub fn accepted(key_id: String, envelope_hash: [u8; 32]) -> Option<Self> {
(!key_id.is_empty() && key_id.len() <= 128 && envelope_hash != [0; 32]).then_some(Self {
key_id,
envelope_hash,
})
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum LearnedArtifactError {
#[error("artifact content hash mismatch")]
HashMismatch,
#[error("artifact manifest is incompatible")]
IncompatibleManifest,
#[error("artifact signature receipt is invalid")]
InvalidSignatureReceipt,
#[error("artifact exceeds the configured byte limit")]
ArtifactTooLarge,
}
/// Verified immutable model bytes and activation provenance.
#[derive(Clone, Debug)]
pub struct LearnedArtifact {
pub content_hash: [u8; 32],
pub manifest: LearnedArtifactManifest,
pub signature: SignatureVerification,
bytes: Vec<u8>,
}
impl LearnedArtifact {
pub fn verified(
bytes: Vec<u8>,
expected: [u8; 32],
manifest: LearnedArtifactManifest,
signature: SignatureVerification,
maximum_bytes: usize,
) -> Result<Self, LearnedArtifactError> {
if bytes.len() > maximum_bytes {
return Err(LearnedArtifactError::ArtifactTooLarge);
}
if !manifest.is_compatible() {
return Err(LearnedArtifactError::IncompatibleManifest);
}
if signature.key_id.is_empty() || signature.envelope_hash == [0; 32] {
return Err(LearnedArtifactError::InvalidSignatureReceipt);
}
let actual = *blake3::hash(&bytes).as_bytes();
if actual != expected {
return Err(LearnedArtifactError::HashMismatch);
}
Ok(Self {
content_hash: actual,
manifest,
signature,
bytes,
})
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
}
/// Learned heads after hard output validation.
#[derive(Clone, Copy, Debug)]
pub struct LearnedResidual {
pub joints_m: [[f32; 3]; 17],
pub log_variance: [[f32; 3]; 17],
pub contact_probability: [f32; 2],
pub abstention_probability: f32,
}
impl LearnedResidual {
/// Reject non-finite output, then clamp residuals and probabilistic heads.
#[must_use]
pub fn bounded(mut self, cap_m: f32) -> Option<Self> {
if !cap_m.is_finite()
|| cap_m <= 0.0
|| !self
.joints_m
.iter()
.flatten()
.all(|value| value.is_finite())
|| !self
.log_variance
.iter()
.flatten()
.all(|value| value.is_finite())
|| !self
.contact_probability
.iter()
.all(|value| value.is_finite())
|| !self.abstention_probability.is_finite()
{
return None;
}
for joint in &mut self.joints_m {
for coordinate in joint {
*coordinate = coordinate.clamp(-cap_m, cap_m);
}
}
for joint in &mut self.log_variance {
for coordinate in joint {
*coordinate = coordinate.clamp(-20.0, 10.0);
}
}
for probability in &mut self.contact_probability {
*probability = probability.clamp(0.0, 1.0);
}
self.abstention_probability = self.abstention_probability.clamp(0.0, 1.0);
Some(self)
}
}

View File

@@ -0,0 +1,145 @@
//! Fixed-width, bounded learned-residual feature encoding.
use std::collections::VecDeque;
use wifi_densepose_core::{ConstraintResiduals, PoseObservationV2};
use crate::constraints::TemporalHistory;
pub const RF_EMBEDDING_WIDTH: usize = 128;
pub const BASE_FEATURE_WIDTH: usize = 319;
pub const FEATURE_WIDTH: usize = BASE_FEATURE_WIDTH + RF_EMBEDDING_WIDTH;
/// Exactly one encoded model timestep.
#[derive(Clone, Debug, PartialEq)]
pub struct PoseFeatureFrame {
values: Vec<f32>,
}
impl PoseFeatureFrame {
/// Encode observer, uncertainty, deterministic residual, temporal, floor,
/// OOD, and optional RF embedding features without allocating unbounded data.
#[must_use]
pub fn encode(
raw: &PoseObservationV2,
residuals: ConstraintResiduals,
history: &TemporalHistory,
rf_embedding: Option<&[f32; RF_EMBEDDING_WIDTH]>,
) -> Self {
let mut values = Vec::with_capacity(FEATURE_WIDTH);
for joint in &raw.joints {
values.extend(joint.position_m);
}
for joint in &raw.joints {
values.extend([
joint.covariance_m2.xx,
joint.covariance_m2.xy,
joint.covariance_m2.xz,
joint.covariance_m2.yy,
joint.covariance_m2.yz,
joint.covariance_m2.zz,
]);
}
values.extend(raw.joints.iter().map(|joint| joint.confidence.get()));
values.extend(
raw.joints
.iter()
.map(|joint| f32::from(joint.visibility as u8)),
);
values.extend([
residuals.bone_m,
residuals.joint_limit_rad,
residuals.velocity_mps,
residuals.acceleration_mps2,
residuals.temporal_jerk,
residuals.floor_penetration_m,
residuals.contact_m,
residuals.collision_m,
residuals.normalized_total,
]);
let current = raw.joints.map(|joint| joint.position_m);
let velocity = history
.previous
.as_ref()
.map_or([[0.0; 3]; 17], |(previous, ns)| {
derivative(&current, previous, raw.timestamp_ns.saturating_sub(*ns))
});
for joint in velocity {
values.extend(joint);
}
let acceleration = match (&history.previous, &history.previous_previous) {
(Some((previous, previous_ns)), Some((older, older_ns))) => {
let previous_velocity =
derivative(previous, older, previous_ns.saturating_sub(*older_ns));
derivative(
&velocity,
&previous_velocity,
raw.timestamp_ns.saturating_sub(*previous_ns),
)
}
_ => [[0.0; 3]; 17],
};
for joint in acceleration {
values.extend(joint);
}
values.extend(raw.joints.iter().map(|joint| {
raw.floor_plane
.map_or(0.0, |floor| floor.signed_distance(joint.position_m))
}));
values.extend(match raw.trust_state {
wifi_densepose_core::PoseTrustState::Known => [1.0, 0.0, 0.0],
wifi_densepose_core::PoseTrustState::Degraded => [0.0, 1.0, 0.0],
wifi_densepose_core::PoseTrustState::Unknown => [0.0, 0.0, 1.0],
});
values.push(raw.observer_confidence.get());
values.extend(rf_embedding.copied().unwrap_or([0.0; RF_EMBEDDING_WIDTH]));
debug_assert_eq!(values.len(), FEATURE_WIDTH);
Self { values }
}
#[must_use]
pub fn values(&self) -> &[f32] {
&self.values
}
}
/// Bounded twenty-frame history used by both trainer and inference.
#[derive(Clone, Debug, Default)]
pub struct FeatureHistory {
frames: VecDeque<PoseFeatureFrame>,
}
impl FeatureHistory {
pub fn push(&mut self, frame: PoseFeatureFrame) {
if self.frames.len() == super::model::HISTORY_FRAMES {
self.frames.pop_front();
}
self.frames.push_back(frame);
}
#[must_use]
pub fn is_ready(&self) -> bool {
self.frames.len() == super::model::HISTORY_FRAMES
}
/// Flatten only a complete history; partial windows must abstain.
#[must_use]
pub fn flattened(&self) -> Option<Vec<f32>> {
self.is_ready().then(|| {
self.frames
.iter()
.flat_map(|frame| frame.values.iter().copied())
.collect()
})
}
}
fn derivative(current: &[[f32; 3]; 17], previous: &[[f32; 3]; 17], dt_ns: u64) -> [[f32; 3]; 17] {
let dt = dt_ns as f32 / 1_000_000_000.0;
if dt <= 0.0 {
return [[0.0; 3]; 17];
}
core::array::from_fn(|joint| {
core::array::from_fn(|axis| (current[joint][axis] - previous[joint][axis]) / dt)
})
}

View File

@@ -0,0 +1,23 @@
//! Optional Burn residual artifact boundary.
//!
//! Model training and activation remain evidence-gated. This module enforces
//! signed/hash-addressed artifact identity and the deterministic correction cap.
mod artifact;
pub mod features;
pub mod model;
#[cfg(feature = "learned-cpu")]
pub mod runtime;
pub mod training;
pub use artifact::{
LearnedArtifact, LearnedArtifactError, LearnedArtifactManifest, LearnedResidual,
SignatureVerification,
};
/// Re-export the selected framework so downstream trainer crates use the exact
/// workspace-pinned Burn version rather than introducing a second runtime.
pub use burn_core as framework;
#[cfg(feature = "learned-cpu")]
pub use burn_ndarray as cpu_backend;
pub use burn_nn as neural_network;

View File

@@ -0,0 +1,93 @@
//! Burn GRU residual architecture. Backend selection remains a Cargo feature.
use burn::{
module::Module,
record::{FullPrecisionSettings, NamedMpkBytesRecorder, Recorder, RecorderError},
tensor::{backend::Backend, Tensor},
};
use burn_core as burn;
use burn_nn::{Gru, GruConfig, Linear, LinearConfig, Sigmoid, Tanh};
use super::features::FEATURE_WIDTH;
pub const HISTORY_FRAMES: usize = 20;
pub const HIDDEN_WIDTH: usize = 128;
pub const JOINT_RESIDUAL_WIDTH: usize = 17 * 3;
/// Multi-head outputs for bounded residuals, log variance, contact, and abstention.
pub struct ResidualModelOutput<B: Backend> {
pub joint_residual: Tensor<B, 2>,
pub residual_log_variance: Tensor<B, 2>,
pub contact_probability: Tensor<B, 2>,
pub abstention_probability: Tensor<B, 2>,
}
/// Two-layer unidirectional GRU followed by independent auditable heads.
#[derive(Module, Debug)]
pub struct ResidualGru<B: Backend> {
gru1: Gru<B>,
gru2: Gru<B>,
residual_head: Linear<B>,
variance_head: Linear<B>,
contact_head: Linear<B>,
abstention_head: Linear<B>,
tanh: Tanh,
sigmoid: Sigmoid,
}
impl<B: Backend> ResidualGru<B> {
/// Initialize the ADR-323 architecture on an explicit backend device.
#[must_use]
pub fn init(device: &B::Device) -> Self {
Self {
gru1: GruConfig::new(FEATURE_WIDTH, HIDDEN_WIDTH, true)
.with_clip(Some(10.0))
.init(device),
gru2: GruConfig::new(HIDDEN_WIDTH, HIDDEN_WIDTH, true)
.with_clip(Some(10.0))
.init(device),
residual_head: LinearConfig::new(HIDDEN_WIDTH, JOINT_RESIDUAL_WIDTH).init(device),
variance_head: LinearConfig::new(HIDDEN_WIDTH, JOINT_RESIDUAL_WIDTH).init(device),
contact_head: LinearConfig::new(HIDDEN_WIDTH, 2).init(device),
abstention_head: LinearConfig::new(HIDDEN_WIDTH, 1).init(device),
tanh: Tanh::new(),
sigmoid: Sigmoid::new(),
}
}
/// Deserialize only bytes that already passed manifest, hash, and
/// signature-receipt verification at the artifact boundary.
pub fn from_verified_artifact(
artifact: &super::LearnedArtifact,
device: &B::Device,
) -> Result<Self, RecorderError> {
let recorder = NamedMpkBytesRecorder::<FullPrecisionSettings>::default();
let record = recorder.load(artifact.bytes().to_vec(), device)?;
Ok(Self::init(device).load_record(record))
}
/// Serialize a trainer-produced record in the exact activation format.
pub fn into_artifact_bytes(self) -> Result<Vec<u8>, RecorderError> {
NamedMpkBytesRecorder::<FullPrecisionSettings>::default().record(self.into_record(), ())
}
/// Forward `[batch, 20, FEATURE_WIDTH]` to four bounded output heads.
#[must_use]
pub fn forward(&self, input: Tensor<B, 3>) -> ResidualModelOutput<B> {
let hidden = self.gru2.forward(self.gru1.forward(input, None), None);
let [batch, sequence, width] = hidden.shape().dims();
debug_assert_eq!(sequence, HISTORY_FRAMES);
debug_assert_eq!(width, HIDDEN_WIDTH);
let last = hidden
.slice([0..batch, (sequence - 1)..sequence, 0..width])
.squeeze_dim::<2>(1);
ResidualModelOutput {
joint_residual: self.tanh.forward(self.residual_head.forward(last.clone())),
residual_log_variance: self.variance_head.forward(last.clone()),
contact_probability: self
.sigmoid
.forward(self.contact_head.forward(last.clone())),
abstention_probability: self.sigmoid.forward(self.abstention_head.forward(last)),
}
}
}

View File

@@ -0,0 +1,88 @@
//! CPU inference activation for verified Burn artifacts.
use burn_core::tensor::{Tensor, TensorData};
use burn_ndarray::{NdArray, NdArrayDevice};
use super::{
features::FEATURE_WIDTH,
model::{ResidualGru, HISTORY_FRAMES},
LearnedArtifact, LearnedResidual,
};
type CpuBackend = NdArray<f32>;
/// Native CPU residual inference. Construction requires a verified artifact.
pub struct CpuResidualRuntime {
model: ResidualGru<CpuBackend>,
device: NdArrayDevice,
artifact_hash: [u8; 32],
}
impl CpuResidualRuntime {
/// Atomically deserialize a verified Burn record before activation.
pub fn activate(artifact: &LearnedArtifact) -> Result<Self, burn_core::record::RecorderError> {
let device = NdArrayDevice::default();
let model = ResidualGru::from_verified_artifact(artifact, &device)?;
Ok(Self {
model,
device,
artifact_hash: artifact.content_hash,
})
}
/// Active immutable artifact identity.
#[must_use]
pub const fn artifact_hash(&self) -> [u8; 32] {
self.artifact_hash
}
/// Infer one complete history. Partial, non-finite, or malformed windows
/// abstain before entering Burn.
#[must_use]
pub fn predict(
&self,
flattened_history: &[f32],
correction_cap_m: f32,
) -> Option<LearnedResidual> {
if flattened_history.len() != HISTORY_FRAMES * FEATURE_WIDTH
|| !flattened_history.iter().all(|value| value.is_finite())
{
return None;
}
let input = Tensor::<CpuBackend, 3>::from_data(
TensorData::new(
flattened_history.to_vec(),
[1, HISTORY_FRAMES, FEATURE_WIDTH],
),
&self.device,
);
let output = self.model.forward(input);
let residual = output.joint_residual.into_data().to_vec::<f32>().ok()?;
let log_variance = output
.residual_log_variance
.into_data()
.to_vec::<f32>()
.ok()?;
let contact = output
.contact_probability
.into_data()
.to_vec::<f32>()
.ok()?;
let abstention = output
.abstention_probability
.into_data()
.to_vec::<f32>()
.ok()?;
let candidate = LearnedResidual {
joints_m: core::array::from_fn(|joint| {
core::array::from_fn(|axis| residual[joint * 3 + axis] * correction_cap_m)
}),
log_variance: core::array::from_fn(|joint| {
core::array::from_fn(|axis| log_variance[joint * 3 + axis])
}),
contact_probability: [contact[0], contact[1]],
abstention_probability: abstention[0],
};
candidate.bounded(correction_cap_m)
}
}

View File

@@ -0,0 +1,38 @@
//! Auditable supervised residual objective shared by trainers and reports.
/// Unweighted components reported for every training/evaluation batch.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct LossComponents {
pub uncertainty_weighted_mpjpe: f32,
pub bone: f32,
pub temporal_jerk: f32,
pub contact: f32,
pub uncertainty_calibration: f32,
pub intervention: f32,
}
impl LossComponents {
/// ADR-323 supervised objective. Non-finite components are rejected.
#[must_use]
pub fn total(self) -> Option<f32> {
let values = [
self.uncertainty_weighted_mpjpe,
self.bone,
self.temporal_jerk,
self.contact,
self.uncertainty_calibration,
self.intervention,
];
values
.iter()
.all(|value| value.is_finite() && *value >= 0.0)
.then_some({
self.uncertainty_weighted_mpjpe
+ 0.20 * self.bone
+ 0.10 * self.temporal_jerk
+ 0.10 * self.contact
+ 0.10 * self.uncertainty_calibration
+ 0.05 * self.intervention
})
}
}

View File

@@ -0,0 +1,25 @@
//! Native Rust physics-constrained pose assessment (ADR-323).
//!
//! The default feature contains only deterministic kinematic code. Optional
//! dynamics and learned backends cannot override hard validation, correction,
//! provenance, or confidence invariants.
#![forbid(unsafe_code)]
#![allow(missing_docs)]
pub mod config;
pub mod constraints;
#[cfg(feature = "dynamics")]
pub mod dynamics;
pub mod engine;
pub mod error;
#[cfg(feature = "learned")]
pub mod learned;
pub mod metrics;
pub mod projector;
pub mod skeleton;
pub mod uncertainty;
pub use config::PhysicsConfig;
pub use engine::{CorrectionAuthorization, PhysicsEngine, VerifiedFrameContext};
pub use error::PhysicsError;

View File

@@ -0,0 +1,154 @@
//! Privacy-safe metric labels and bounded in-process aggregation.
use std::{collections::BTreeMap, fmt::Write as _, sync::Mutex};
use wifi_densepose_core::{PoseRefinementV1, RefinementDisposition};
/// Fixed allowlisted scalars. It deliberately contains no joint, room, or ID data.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MetricSnapshot {
pub latency_seconds: f64,
pub solver_iterations: u8,
pub max_correction_meters: f32,
pub bone_residual: f32,
pub floor_penetration_meters: f32,
pub temporal_jerk: f32,
pub confidence_delta: f32,
}
#[derive(Default)]
struct Aggregate {
frames: BTreeMap<(String, String, String), u64>,
total_latency_seconds: f64,
observations: u64,
deadline_exceeded: u64,
invalid_inputs: u64,
track_resets: BTreeMap<String, u64>,
dropped: u64,
latest: Option<MetricSnapshot>,
}
/// Thread-safe, bounded metrics registry with no person, joint, or room labels.
#[derive(Default)]
pub struct PhysicsMetrics {
aggregate: Mutex<Aggregate>,
}
impl PhysicsMetrics {
/// Record one returned result. The label cardinality is bounded by enums.
pub fn observe(&self, result: &PoseRefinementV1, observer_confidence: f32) {
let snapshot = MetricSnapshot::from_result(result, observer_confidence);
let mut aggregate = self
.aggregate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let key = (
enum_label(result.mode),
enum_label(result.disposition),
result.reason.map_or_else(|| "none".into(), enum_label),
);
*aggregate.frames.entry(key).or_default() += 1;
aggregate.total_latency_seconds += snapshot.latency_seconds;
aggregate.observations += 1;
if result.reason == Some(wifi_densepose_core::AbstentionReason::DeadlineExceeded) {
aggregate.deadline_exceeded += 1;
}
if result.disposition == RefinementDisposition::Rejected {
aggregate.invalid_inputs += 1;
}
aggregate.latest = Some(snapshot);
}
/// Record a bounded internal track reset reason.
pub fn track_reset(&self, reason: &'static str) {
let mut aggregate = self
.aggregate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*aggregate.track_resets.entry(reason.into()).or_default() += 1;
}
/// Record discarded intermediate refinement work under backpressure.
pub fn dropped_intermediate(&self) {
let mut aggregate = self
.aggregate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
aggregate.dropped += 1;
}
/// Encode the fixed metric allowlist in Prometheus text format.
#[must_use]
pub fn encode_prometheus(&self) -> String {
let aggregate = self
.aggregate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut output = String::with_capacity(2048);
for ((mode, disposition, reason), count) in &aggregate.frames {
writeln!(
output,
"ruview_pose_physics_frames_total{{mode=\"{mode}\",disposition=\"{disposition}\",reason=\"{reason}\"}} {count}"
)
.expect("writing to String cannot fail");
}
let average_latency = if aggregate.observations == 0 {
0.0
} else {
aggregate.total_latency_seconds / aggregate.observations as f64
};
writeln!(
output,
"ruview_pose_physics_latency_seconds{{stage=\"total\",target=\"local\"}} {average_latency}"
)
.expect("writing to String cannot fail");
if let Some(latest) = aggregate.latest {
write!(
output,
"ruview_pose_physics_solver_iterations{{engine=\"kinematic-pbd\"}} {}\nruview_pose_physics_max_correction_meters {}\nruview_pose_physics_bone_residual {}\nruview_pose_physics_floor_penetration_meters {}\nruview_pose_physics_temporal_jerk {}\nruview_pose_physics_confidence_delta {}\nruview_pose_physics_raw_refined_divergence_meters {}\n",
latest.solver_iterations,
latest.max_correction_meters,
latest.bone_residual,
latest.floor_penetration_meters,
latest.temporal_jerk,
latest.confidence_delta,
latest.max_correction_meters,
)
.expect("writing to String cannot fail");
}
for (reason, count) in &aggregate.track_resets {
writeln!(
output,
"ruview_pose_physics_track_resets_total{{reason=\"{reason}\"}} {count}"
)
.expect("writing to String cannot fail");
}
write!(
output,
"ruview_pose_physics_invalid_input_total{{reason=\"contract\"}} {}\nruview_pose_physics_deadline_exceeded_total{{stage=\"total\"}} {}\nruview_pose_physics_dropped_intermediate_total {}\n",
aggregate.invalid_inputs, aggregate.deadline_exceeded, aggregate.dropped,
)
.expect("writing to String cannot fail");
output
}
}
fn enum_label(value: impl core::fmt::Debug) -> String {
format!("{value:?}").to_ascii_lowercase()
}
impl MetricSnapshot {
/// Extract only approved aggregate scalars.
#[must_use]
pub fn from_result(result: &PoseRefinementV1, observer_confidence: f32) -> Self {
Self {
latency_seconds: result.intervention.elapsed_us as f64 / 1_000_000.0,
solver_iterations: result.intervention.solver_iterations,
max_correction_meters: result.intervention.max_joint_correction_m,
bone_residual: result.residuals.bone_m,
floor_penetration_meters: result.residuals.floor_penetration_m,
temporal_jerk: result.residuals.temporal_jerk,
confidence_delta: result.effective_confidence.get() - observer_confidence,
}
}
}

View File

@@ -0,0 +1,5 @@
//! Bounded position-based projection.
mod pbd;
pub use pbd::{project, Projection};

View File

@@ -0,0 +1,213 @@
//! Covariance-weighted deterministic position-based dynamics projector.
use std::time::{Duration, Instant};
use nalgebra::Vector3;
use wifi_densepose_core::{FloorPlane, InterventionSummary};
use crate::{
config::PhysicsConfig,
constraints::{bone_residual, project_floor, project_joint_limits, TemporalHistory},
skeleton::{
distance, virtual_joints, SkeletonPosterior, OBSERVED_EDGES, VIRTUAL_NECK_EDGE,
VIRTUAL_TRUNK_EDGE,
},
};
/// Successful bounded candidate.
#[derive(Clone, Debug)]
pub struct Projection {
pub joints: [[f32; 3]; 17],
pub intervention: InterventionSummary,
}
/// Project distances, temporal motion, and floor penetration within a deadline.
#[allow(clippy::too_many_arguments)]
pub fn project(
observed: &[[f32; 3]; 17],
weights: &[f32; 17],
posterior: &SkeletonPosterior,
history: &TemporalHistory,
timestamp_ns: u64,
floor: FloorPlane,
config: &PhysicsConfig,
started: Instant,
) -> Result<Projection, wifi_densepose_core::AbstentionReason> {
let mut candidate = *observed;
let deadline = Duration::from_millis(config.deadline_ms);
let mut iterations = 0u8;
let mut previous_residual = f32::INFINITY;
for iteration in 0..config.max_iterations {
if started.elapsed() >= deadline {
return Err(wifi_densepose_core::AbstentionReason::DeadlineExceeded);
}
for (edge, (parent, child)) in OBSERVED_EDGES.iter().copied().enumerate() {
let delta = Vector3::from(candidate[child]) - Vector3::from(candidate[parent]);
let length = delta.norm();
if length <= 1.0e-6 {
continue;
}
let error = length - posterior.target(edge, length);
let direction = delta / length;
let parent_weight = weights[parent];
let child_weight = weights[child];
let total_weight = (parent_weight + child_weight).max(1.0e-6);
let parent_move = direction * error * parent_weight / total_weight;
let child_move = direction * error * child_weight / total_weight;
candidate[parent] = (Vector3::from(candidate[parent]) + parent_move).into();
candidate[child] = (Vector3::from(candidate[child]) - child_move).into();
}
project_virtual_edges(&mut candidate, weights, posterior);
project_joint_limits(&mut candidate, weights);
project_temporal(&mut candidate, history, timestamp_ns, config);
project_floor(&mut candidate, floor, weights);
iterations = iteration + 1;
let residual = bone_residual(&candidate, posterior);
if (previous_residual - residual).abs() < config.solver_epsilon_m {
break;
}
previous_residual = residual;
}
let mut max_correction = 0.0f32;
let mut corrected = 0u8;
for (before, after) in observed.iter().zip(candidate.iter()) {
let correction = distance(*before, *after);
max_correction = max_correction.max(correction);
corrected += u8::from(correction > config.solver_epsilon_m);
}
let root_before = midpoint(observed[11], observed[12]);
let root_after = midpoint(candidate[11], candidate[12]);
let root_correction = distance(root_before, root_after);
if max_correction > config.max_joint_correction_m
|| root_correction > config.max_root_correction_m
{
return Err(wifi_densepose_core::AbstentionReason::CorrectionTooLarge);
}
Ok(Projection {
joints: candidate,
intervention: InterventionSummary {
max_joint_correction_m: max_correction,
root_correction_m: root_correction,
corrected_joint_count: corrected,
solver_iterations: iterations,
elapsed_us: elapsed_us(started),
},
})
}
fn project_virtual_edges(
joints: &mut [[f32; 3]; 17],
weights: &[f32; 17],
posterior: &SkeletonPosterior,
) {
let (pelvis, thorax) = virtual_joints(joints);
project_group_edge(
joints,
weights,
&[11, 12],
&[5, 6],
pelvis,
thorax,
posterior.target(VIRTUAL_TRUNK_EDGE, distance(pelvis, thorax)),
);
let (_, thorax) = virtual_joints(joints);
let nose = joints[0];
project_group_edge(
joints,
weights,
&[5, 6],
&[0],
thorax,
nose,
posterior.target(VIRTUAL_NECK_EDGE, distance(thorax, nose)),
);
}
#[allow(clippy::too_many_arguments)]
fn project_group_edge(
joints: &mut [[f32; 3]; 17],
weights: &[f32; 17],
parent_indices: &[usize],
child_indices: &[usize],
parent_point: [f32; 3],
child_point: [f32; 3],
target: f32,
) {
let delta = Vector3::from(child_point) - Vector3::from(parent_point);
let length = delta.norm();
if length <= 1.0e-6 {
return;
}
let error = length - target;
let direction = delta / length;
let parent_weight =
parent_indices.iter().map(|&i| weights[i]).sum::<f32>() / parent_indices.len() as f32;
let child_weight =
child_indices.iter().map(|&i| weights[i]).sum::<f32>() / child_indices.len() as f32;
let total = (parent_weight + child_weight).max(1.0e-6);
let parent_move = direction * error * parent_weight / total;
let child_move = direction * error * child_weight / total;
for &index in parent_indices {
joints[index] = (Vector3::from(joints[index]) + parent_move).into();
}
for &index in child_indices {
joints[index] = (Vector3::from(joints[index]) - child_move).into();
}
}
fn project_temporal(
candidate: &mut [[f32; 3]; 17],
history: &TemporalHistory,
timestamp_ns: u64,
config: &PhysicsConfig,
) {
let Some((previous, previous_ns)) = &history.previous else {
return;
};
let dt = timestamp_ns.saturating_sub(*previous_ns) as f32 / 1_000_000_000.0;
if dt <= 0.0 || dt * 1000.0 > config.max_frame_gap_ms as f32 {
return;
}
for (index, (joint, prior)) in candidate.iter_mut().zip(previous).enumerate() {
let mut velocity = (Vector3::from(*joint) - Vector3::from(*prior)) / dt;
let speed = velocity.norm();
if speed > config.max_velocity_mps {
velocity *= config.max_velocity_mps / speed;
}
if let Some((older, older_ns)) = &history.previous_previous {
let previous_dt = previous_ns.saturating_sub(*older_ns) as f32 / 1_000_000_000.0;
if previous_dt > 0.0 {
let previous_velocity =
(Vector3::from(previous[index]) - Vector3::from(older[index])) / previous_dt;
let delta_velocity = velocity - previous_velocity;
let maximum_delta = config.max_acceleration_mps2 * dt;
if delta_velocity.norm() > maximum_delta {
velocity = previous_velocity + delta_velocity.normalize() * maximum_delta;
}
}
}
*joint = (Vector3::from(*prior) + velocity * dt).into();
}
}
fn midpoint(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
(a[0] + b[0]) * 0.5,
(a[1] + b[1]) * 0.5,
(a[2] + b[2]) * 0.5,
]
}
fn elapsed_us(started: Instant) -> u64 {
#[cfg(feature = "deterministic")]
{
let _ = started;
0
}
#[cfg(not(feature = "deterministic"))]
{
started.elapsed().as_micros().try_into().unwrap_or(u64::MAX)
}
}

View File

@@ -0,0 +1,138 @@
//! COCO-17 skeleton topology and anonymous track-scoped bone posteriors.
use nalgebra::Vector3;
use smallvec::SmallVec;
/// Observed parent-child constraints. Virtual pelvis/thorax constraints are
/// evaluated separately so virtual joints can never be mislabeled observed.
pub const OBSERVED_EDGES: [(usize, usize); 14] = [
(11, 12),
(5, 6),
(11, 13),
(13, 15),
(12, 14),
(14, 16),
(5, 7),
(7, 9),
(6, 8),
(8, 10),
(0, 1),
(1, 3),
(0, 2),
(2, 4),
];
/// Fourteen observed edges plus pelvis↔thorax and thorax↔nose virtual edges.
pub const CONSTRAINED_EDGE_COUNT: usize = OBSERVED_EDGES.len() + 2;
/// Posterior index for the derived pelvis↔thorax edge.
pub const VIRTUAL_TRUNK_EDGE: usize = OBSERVED_EDGES.len();
/// Posterior index for the derived thorax↔nose edge.
pub const VIRTUAL_NECK_EDGE: usize = OBSERVED_EDGES.len() + 1;
/// Anonymous, memory-only per-track length estimate.
#[derive(Clone, Debug)]
pub struct SkeletonPosterior {
samples_m: [SmallVec<[f32; 9]>; CONSTRAINED_EDGE_COUNT],
}
impl Default for SkeletonPosterior {
fn default() -> Self {
Self {
samples_m: core::array::from_fn(|_| SmallVec::new()),
}
}
}
impl SkeletonPosterior {
/// Update only edges whose endpoints pass the high-confidence gate.
pub fn observe(&mut self, joints: &[[f32; 3]; 17], confidence: &[f32; 17], threshold: f32) {
for (index, (a, b)) in OBSERVED_EDGES.iter().copied().enumerate() {
if confidence[a] < threshold || confidence[b] < threshold {
continue;
}
let length = distance(joints[a], joints[b]);
if !length.is_finite() || !(0.01..=2.5).contains(&length) {
continue;
}
self.observe_sample(index, length);
}
let (pelvis, thorax) = virtual_joints(joints);
let trunk_confidence = confidence[11]
.min(confidence[12])
.min(confidence[5])
.min(confidence[6]);
if trunk_confidence >= threshold {
self.observe_sample(VIRTUAL_TRUNK_EDGE, distance(pelvis, thorax));
}
let neck_confidence = confidence[5].min(confidence[6]).min(confidence[0]);
if neck_confidence >= threshold {
self.observe_sample(VIRTUAL_NECK_EDGE, distance(thorax, joints[0]));
}
}
/// Return the posterior target, or the current length before calibration.
#[must_use]
pub fn target(&self, edge: usize, current: f32) -> f32 {
let samples = &self.samples_m[edge];
if samples.len() < 3 {
current
} else {
median(samples)
}
}
fn observe_sample(&mut self, edge: usize, length: f32) {
if !length.is_finite() || !(0.01..=2.5).contains(&length) {
return;
}
let samples = &mut self.samples_m[edge];
if samples.len() >= 3 {
let center = median(samples);
let mut deviations = SmallVec::<[f32; 9]>::new();
deviations.extend(samples.iter().map(|sample| (sample - center).abs()));
let mad = median(&deviations);
let tolerance = (center * 0.15).max(mad * 4.0).max(0.02);
if (length - center).abs() > tolerance {
return;
}
}
if samples.len() == 9 {
samples.remove(0);
}
samples.push(length);
}
}
fn median(samples: &[f32]) -> f32 {
let mut sorted = SmallVec::<[f32; 9]>::from_slice(samples);
sorted.sort_unstable_by(f32::total_cmp);
let middle = sorted.len() / 2;
if sorted.len() % 2 == 0 {
(sorted[middle - 1] + sorted[middle]) * 0.5
} else {
sorted[middle]
}
}
/// Euclidean distance between points.
#[must_use]
pub fn distance(a: [f32; 3], b: [f32; 3]) -> f32 {
(Vector3::from(b) - Vector3::from(a)).norm()
}
/// Derived pelvis and thorax, never exposed as observations.
#[must_use]
pub fn virtual_joints(joints: &[[f32; 3]; 17]) -> ([f32; 3], [f32; 3]) {
(
midpoint(joints[11], joints[12]),
midpoint(joints[5], joints[6]),
)
}
fn midpoint(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
(a[0] + b[0]) * 0.5,
(a[1] + b[1]) * 0.5,
(a[2] + b[2]) * 0.5,
]
}

View File

@@ -0,0 +1,9 @@
//! Covariance-derived movement weights.
use wifi_densepose_core::SymmetricCovariance3;
/// Larger uncertainty permits more motion, bounded away from zero and one.
#[must_use]
pub fn movement_weight(covariance: SymmetricCovariance3) -> f32 {
(covariance.trace() / 0.03).clamp(0.05, 0.95)
}

View File

@@ -0,0 +1,77 @@
use wifi_densepose_core::{
CalibrationId, Coco17Joint, FloorPlane, JointObservation, JointVisibility, ModelRef,
PoseDimensionality, PoseObservationV2, PoseTrustState, Probability, SourceProvenance,
SpatialFrameRef, SymmetricCovariance3, TrackId,
};
pub fn observation(sequence: u64) -> PoseObservationV2 {
let positions = [
[0.0, 0.0, 1.70],
[-0.03, 0.0, 1.73],
[0.03, 0.0, 1.73],
[-0.08, 0.0, 1.71],
[0.08, 0.0, 1.71],
[-0.20, 0.0, 1.45],
[0.20, 0.0, 1.45],
[-0.35, 0.0, 1.15],
[0.35, 0.0, 1.15],
[-0.45, 0.0, 0.90],
[0.45, 0.0, 0.90],
[-0.14, 0.0, 0.90],
[0.14, 0.0, 0.90],
[-0.14, 0.0, 0.48],
[0.14, 0.0, 0.48],
[-0.14, 0.0, 0.04],
[0.14, 0.0, 0.04],
];
let joints = core::array::from_fn(|index| JointObservation {
kind: Coco17Joint::ALL[index],
position_m: positions[index],
covariance_m2: SymmetricCovariance3 {
xx: 0.001,
xy: 0.0,
xz: 0.0,
yy: 0.001,
yz: 0.0,
zz: 0.001,
},
confidence: Probability::new(0.8).unwrap(),
visibility: JointVisibility::Visible,
});
let mut raw = PoseObservationV2 {
schema_version: 2,
timestamp_ns: 1_000_000_000 + sequence * 33_000_000,
sensor_epoch: 7,
sequence,
track_id: TrackId("local:7".into()),
frame: SpatialFrameRef {
name: "room:lab".into(),
version: 1,
metric: true,
right_handed: true,
z_up: true,
},
calibration_id: CalibrationId("cal:1".into()),
floor_plane: Some(FloorPlane {
normal: [0.0, 0.0, 1.0],
offset_m: 0.0,
}),
model: ModelRef {
id: "pose:test".into(),
artifact_hash: [3; 32],
},
source: SourceProvenance {
sensor_id: "sensor:1".into(),
authenticated: true,
replay_protected: true,
},
trust_state: PoseTrustState::Known,
dimensionality: PoseDimensionality::Metric3d,
uncertainty_calibrated: true,
joints,
observer_confidence: Probability::new(0.78).unwrap(),
canonical_hash: [0; 32],
};
raw.seal();
raw
}

View File

@@ -0,0 +1,169 @@
#![allow(missing_docs)]
mod common;
use common::observation;
use wifi_densepose_core::{AbstentionReason, PhysicsMode, RefinementDisposition};
use wifi_densepose_physics::{
CorrectionAuthorization, PhysicsConfig, PhysicsEngine, VerifiedFrameContext,
};
#[test]
fn raw_hash_is_preserved_and_confidence_never_increases() {
let raw = observation(1);
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::ShadowCorrect,
..PhysicsConfig::default()
})
.unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
assert_eq!(result.raw_observation_hash, raw.canonical_hash);
assert!(result.effective_confidence.get() <= raw.observer_confidence.get());
assert!(!result.selected);
}
#[test]
fn mode_off_is_identity_and_clears_selection() {
let raw = observation(1);
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::Off,
..PhysicsConfig::default()
})
.unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
assert_eq!(result.disposition, RefinementDisposition::Bypassed);
assert_eq!(result.reason, Some(AbstentionReason::ModeOff));
assert!(result.refined_joints_m.is_none());
assert_eq!(result.effective_confidence, raw.observer_confidence);
}
#[test]
fn unauthenticated_source_cannot_select_correction() {
let mut raw = observation(1);
raw.source.authenticated = false;
raw.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::OptInCorrect,
..PhysicsConfig::default()
})
.unwrap();
engine
.authorize_correction(
CorrectionAuthorization::from_verified_evidence(engine.config_hash(), [7; 32]).unwrap(),
)
.unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
assert_eq!(result.reason, Some(AbstentionReason::SourceUnauthenticated));
assert!(!result.selected);
}
#[test]
fn correction_mode_requires_evidence_authorization() {
let raw = observation(1);
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::OptInCorrect,
..PhysicsConfig::default()
})
.unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
assert_eq!(
result.reason,
Some(AbstentionReason::CorrectionNotAuthorized)
);
assert!(!result.selected);
}
#[test]
fn verified_frame_and_release_receipts_allow_bounded_selection() {
let raw = observation(1);
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::OptInCorrect,
..PhysicsConfig::default()
})
.unwrap();
engine
.authorize_correction(
CorrectionAuthorization::from_verified_evidence(engine.config_hash(), [7; 32]).unwrap(),
)
.unwrap();
let receipt = VerifiedFrameContext::from_verified_envelope(
raw.canonical_hash,
raw.sensor_epoch,
raw.sequence,
raw.source.sensor_id.clone(),
raw.calibration_id.0.clone(),
[8; 32],
)
.unwrap();
let result = engine.process_verified(&raw, raw.timestamp_ns, &receipt);
assert!(result.selected);
}
#[test]
fn audit_reports_raw_residuals_without_running_projector() {
let mut raw = observation(1);
raw.joints[15].position_m[2] = -0.04;
raw.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
assert_eq!(result.disposition, RefinementDisposition::Audited);
assert!(result.residuals.floor_penetration_m >= 0.039);
assert_eq!(result.intervention.solver_iterations, 0);
assert!(result.refined_residuals.is_none());
}
#[test]
fn result_hash_is_canonical_and_detects_changes() {
let raw = observation(1);
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
assert_eq!(result.compute_canonical_hash(), result.canonical_hash);
let mut changed = result.clone();
changed.effective_confidence = wifi_densepose_core::Probability::ZERO;
assert_ne!(changed.compute_canonical_hash(), result.canonical_hash);
}
#[test]
fn correction_mode_transition_is_atomic_and_evidence_bound() {
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
assert!(engine.set_mode(PhysicsMode::DefaultCorrect).is_err());
assert_eq!(engine.config().mode, PhysicsMode::Audit);
let target_hash = engine.config_hash_for_mode(PhysicsMode::OptInCorrect);
engine
.authorize_correction(
CorrectionAuthorization::from_verified_evidence(target_hash, [9; 32]).unwrap(),
)
.unwrap();
engine.set_mode(PhysicsMode::OptInCorrect).unwrap();
assert_eq!(engine.config().mode, PhysicsMode::OptInCorrect);
}
#[test]
fn duplicate_is_idempotent_but_different_content_is_replay_rejected() {
let raw = observation(1);
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
let first = engine.process(&raw, raw.timestamp_ns);
assert_eq!(first, engine.process(&raw, raw.timestamp_ns));
let mut tampered = raw.clone();
tampered.joints[0].position_m[0] += 0.01;
tampered.seal();
assert_eq!(
engine.process(&tampered, tampered.timestamp_ns).reason,
Some(AbstentionReason::ReplayRejected)
);
}
#[test]
fn calibration_change_discards_track_temporal_state() {
let first = observation(1);
let mut second = observation(2);
for joint in &mut second.joints {
joint.position_m[0] += 0.20;
}
second.calibration_id.0 = "cal:2".into();
second.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
let _ = engine.process(&first, first.timestamp_ns);
let result = engine.process(&second, second.timestamp_ns);
assert!(result.residuals.velocity_mps.abs() <= f32::EPSILON);
assert!(result.residuals.acceleration_mps2.abs() <= f32::EPSILON);
}

View File

@@ -0,0 +1,26 @@
#![allow(missing_docs)]
#![cfg(feature = "deterministic")]
mod common;
use common::observation;
use wifi_densepose_core::PhysicsMode;
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
#[test]
fn identical_replay_has_identical_canonical_json() {
let raw = observation(1);
let config = PhysicsConfig {
mode: PhysicsMode::ShadowCorrect,
..PhysicsConfig::default()
};
let a = PhysicsEngine::new(config.clone())
.unwrap()
.process(&raw, raw.timestamp_ns);
let b = PhysicsEngine::new(config)
.unwrap()
.process(&raw, raw.timestamp_ns);
assert_eq!(
serde_json::to_vec(&a).unwrap(),
serde_json::to_vec(&b).unwrap()
);
}

View File

@@ -0,0 +1,56 @@
#![allow(missing_docs)]
#![cfg(feature = "dynamics")]
mod common;
use common::observation;
use wifi_densepose_core::PhysicsMode;
use wifi_densepose_physics::dynamics::DynamicsAuditor;
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
#[test]
fn articulated_audit_is_bounded_and_finite() {
let raw = observation(1);
let joints = raw.joints.map(|joint| joint.position_m);
let mut auditor = DynamicsAuditor::new(&joints, raw.floor_plane.unwrap());
let assessment = auditor.audit(&joints, 1.0 / 30.0, 2);
assert!(assessment.stable);
assert_eq!(assessment.segment_count, 14);
assert!(assessment.joint_count >= 10);
assert_eq!(assessment.substeps, 2);
assert!(assessment.max_tracking_error_m.is_finite());
}
#[test]
fn prone_pose_remains_a_valid_dynamics_input() {
let raw = observation(1);
let mut joints = raw.joints.map(|joint| joint.position_m);
for joint in &mut joints {
joint[2] = 0.05;
}
let mut auditor = DynamicsAuditor::new(&joints, raw.floor_plane.unwrap());
let assessment = auditor.audit(&joints, 1.0 / 30.0, 1);
assert!(assessment.stable);
}
#[test]
fn dynamics_assessment_is_attached_but_never_selects_shadow_output() {
let raw = observation(1);
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::ShadowCorrect,
deadline_ms: 50,
dynamics_audit: true,
..PhysicsConfig::default()
})
.unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
assert!(result.dynamics.is_some());
assert!(!result.selected);
assert_eq!(result.provenance.engine, "kinematic-pbd+rapier-audit");
let mut next = observation(2);
next.joints[9].position_m[0] += 0.01;
next.seal();
let next_result = engine.process(&next, next.timestamp_ns);
assert!(next_result.dynamics.is_some());
assert!(!next_result.selected);
}

View File

@@ -0,0 +1,109 @@
#![allow(missing_docs)]
mod common;
use common::observation;
use wifi_densepose_core::{PhysicsMode, PoseDimensionality, RefinementDisposition};
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
#[test]
fn image_2d_is_audited_but_never_corrected() {
let mut raw = observation(1);
raw.dimensionality = PoseDimensionality::Image2d;
raw.frame.metric = false;
raw.floor_plane = None;
raw.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::OptInCorrect,
..PhysicsConfig::default()
})
.unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
assert_eq!(result.disposition, RefinementDisposition::Audited2d);
assert!(!result.selected && result.refined_joints_m.is_none());
}
#[test]
fn image_pixels_use_a_separate_bounded_coordinate_domain() {
let mut raw = observation(1);
raw.dimensionality = PoseDimensionality::Image2d;
raw.frame.metric = false;
raw.frame.right_handed = false;
raw.frame.z_up = false;
raw.floor_plane = None;
raw.joints[0].position_m = [640.0, 480.0, 0.0];
raw.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
assert_eq!(
engine.process(&raw, raw.timestamp_ns).disposition,
RefinementDisposition::Audited2d
);
raw.sequence += 1;
raw.timestamp_ns += 33_000_000;
raw.joints[0].position_m[0] = 20_000.0;
raw.seal();
assert_eq!(
engine.process(&raw, raw.timestamp_ns).reason,
Some(wifi_densepose_core::AbstentionReason::InvalidNumber)
);
}
#[test]
fn prone_pose_is_not_forced_upright() {
let mut raw = observation(1);
for joint in &mut raw.joints {
joint.position_m[2] = 0.05;
}
raw.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::ShadowCorrect,
..PhysicsConfig::default()
})
.unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
let refined = result.refined_joints_m.unwrap();
assert!(refined.iter().all(|joint| joint[2] < 0.20));
}
#[test]
fn image_2d_audit_tracks_temporal_motion() {
let mut first = observation(1);
first.dimensionality = PoseDimensionality::Image2d;
first.frame.metric = false;
first.floor_plane = None;
first.seal();
let mut second = first.clone();
second.sequence = 2;
second.timestamp_ns += 33_000_000;
second.joints[9].position_m[0] += 0.5;
second.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
let _ = engine.process(&first, first.timestamp_ns);
let result = engine.process(&second, second.timestamp_ns);
assert!(result.residuals.velocity_mps > 0.0);
}
#[test]
fn image_2d_rejects_non_monotonic_sequence_and_time() {
let mut first = observation(2);
first.dimensionality = PoseDimensionality::Image2d;
first.frame.metric = false;
first.frame.right_handed = false;
first.frame.z_up = false;
first.floor_plane = None;
first.uncertainty_calibrated = false;
first.seal();
let mut older = first.clone();
older.sequence = 1;
older.timestamp_ns -= 1;
older.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
assert_eq!(
engine.process(&first, first.timestamp_ns).disposition,
RefinementDisposition::Audited2d
);
assert_eq!(
engine.process(&older, older.timestamp_ns).reason,
Some(wifi_densepose_core::AbstentionReason::NonMonotonicInput)
);
}

View File

@@ -0,0 +1,86 @@
#![allow(missing_docs)]
mod common;
use common::observation;
use wifi_densepose_core::PhysicsMode;
use wifi_densepose_physics::skeleton::SkeletonPosterior;
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
#[test]
fn projector_bounds_vector_acceleration() {
let config = PhysicsConfig {
mode: PhysicsMode::ShadowCorrect,
max_velocity_mps: 4.0,
max_acceleration_mps2: 5.0,
..PhysicsConfig::default()
};
let mut engine = PhysicsEngine::new(config).unwrap();
let first = observation(1);
let _ = engine.process(&first, first.timestamp_ns);
let mut second = observation(2);
for joint in &mut second.joints {
joint.position_m[0] += 0.02;
}
second.seal();
let _ = engine.process(&second, second.timestamp_ns);
let mut third = observation(3);
for joint in &mut third.joints {
joint.position_m[0] -= 0.02;
}
third.seal();
let result = engine.process(&third, third.timestamp_ns);
let refined = result.refined_residuals.expect("shadow candidate");
assert!(refined.acceleration_mps2 <= 5.01, "{refined:?}");
}
#[test]
fn projector_reduces_impossible_elbow_angle() {
let mut raw = observation(1);
raw.joints[9].position_m = [-0.25, 0.0, 1.30];
raw.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::ShadowCorrect,
..PhysicsConfig::default()
})
.unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
let refined = result.refined_residuals.expect("shadow candidate");
assert!(refined.joint_limit_rad < result.residuals.joint_limit_rad);
}
#[test]
fn one_outlier_does_not_redefine_calibrated_bone_length() {
let mut engine = PhysicsEngine::new(PhysicsConfig {
mode: PhysicsMode::ShadowCorrect,
..PhysicsConfig::default()
})
.unwrap();
for sequence in 1..=6 {
let raw = observation(sequence);
let _ = engine.process(&raw, raw.timestamp_ns);
}
let mut outlier = observation(7);
outlier.joints[9].position_m[0] -= 0.14;
outlier.seal();
let result = engine.process(&outlier, outlier.timestamp_ns);
assert!(result.residuals.bone_m > 0.001, "{:?}", result.residuals);
assert!(result.refined_residuals.expect("shadow candidate").bone_m < result.residuals.bone_m);
}
#[test]
fn bone_posterior_rejects_single_calibration_outlier() {
let baseline = observation(1);
let joints = baseline.joints.map(|joint| joint.position_m);
let confidence = [0.9; 17];
let mut posterior = SkeletonPosterior::default();
for _ in 0..6 {
posterior.observe(&joints, &confidence, 0.7);
}
let target = posterior.target(7, 0.0);
let mut outlier = joints;
outlier[9][0] -= 0.5;
posterior.observe(&outlier, &confidence, 0.7);
assert!((posterior.target(7, 0.0) - target).abs() < 0.005);
}

View File

@@ -0,0 +1,186 @@
#![allow(missing_docs)]
#![cfg(feature = "learned")]
mod common;
use common::observation;
use wifi_densepose_core::ConstraintResiduals;
use wifi_densepose_physics::{
constraints::TemporalHistory,
learned::{
features::{FeatureHistory, PoseFeatureFrame, FEATURE_WIDTH},
training::LossComponents,
LearnedArtifact, LearnedArtifactError, LearnedArtifactManifest, LearnedResidual,
SignatureVerification,
},
};
#[test]
fn feature_encoder_is_fixed_width_finite_and_requires_complete_history() {
let raw = observation(1);
let frame = PoseFeatureFrame::encode(
&raw,
ConstraintResiduals::default(),
&TemporalHistory::default(),
None,
);
assert_eq!(frame.values().len(), FEATURE_WIDTH);
assert!(frame.values().iter().all(|value| value.is_finite()));
let mut history = FeatureHistory::default();
for _ in 0..19 {
history.push(frame.clone());
}
assert!(!history.is_ready());
assert!(history.flattened().is_none());
history.push(frame);
assert_eq!(history.flattened().unwrap().len(), 20 * FEATURE_WIDTH);
}
#[test]
fn training_objective_has_declared_weights_and_rejects_invalid_components() {
let components = LossComponents {
uncertainty_weighted_mpjpe: 1.0,
bone: 1.0,
temporal_jerk: 1.0,
contact: 1.0,
uncertainty_calibration: 1.0,
intervention: 1.0,
};
assert!((components.total().unwrap() - 1.55).abs() < 1.0e-6);
assert!(LossComponents {
bone: f32::NAN,
..components
}
.total()
.is_none());
assert!(LossComponents {
intervention: -1.0,
..components
}
.total()
.is_none());
}
#[test]
fn learned_artifact_requires_hash_manifest_size_and_signature_receipts() {
let bytes = b"signed-burn-record".to_vec();
let hash = *blake3::hash(&bytes).as_bytes();
let manifest = LearnedArtifactManifest::adr323("pose-residual-v1".into());
let signature = SignatureVerification::accepted("release-key".into(), [7; 32]).unwrap();
let artifact = LearnedArtifact::verified(
bytes.clone(),
hash,
manifest.clone(),
signature.clone(),
bytes.len(),
)
.unwrap();
assert_eq!(artifact.bytes(), bytes);
assert_eq!(artifact.content_hash, hash);
assert_eq!(
LearnedArtifact::verified(
bytes.clone(),
[9; 32],
manifest.clone(),
signature.clone(),
64
)
.unwrap_err(),
LearnedArtifactError::HashMismatch
);
assert_eq!(
LearnedArtifact::verified(bytes.clone(), hash, manifest.clone(), signature.clone(), 1)
.unwrap_err(),
LearnedArtifactError::ArtifactTooLarge
);
let mut incompatible = manifest;
incompatible.feature_width += 1;
assert_eq!(
LearnedArtifact::verified(bytes, hash, incompatible, signature, 64).unwrap_err(),
LearnedArtifactError::IncompatibleManifest
);
}
#[test]
fn learned_heads_are_finite_and_hard_bounded() {
let residual = LearnedResidual {
joints_m: [[[0.50, -0.50, 0.01]; 17][0]; 17],
log_variance: [[[-40.0, 40.0, 0.0]; 17][0]; 17],
contact_probability: [-1.0, 2.0],
abstention_probability: 1.5,
}
.bounded(0.20)
.unwrap();
assert!(residual
.joints_m
.iter()
.flatten()
.all(|coordinate| coordinate.abs() <= 0.20));
assert!(residual
.contact_probability
.iter()
.zip([0.0, 1.0])
.all(|(actual, expected)| (actual - expected).abs() <= f32::EPSILON));
assert!((residual.abstention_probability - 1.0).abs() <= f32::EPSILON);
assert!(residual.log_variance[0]
.iter()
.zip([-20.0, 10.0, 0.0])
.all(|(actual, expected)| (actual - expected).abs() <= f32::EPSILON));
let mut invalid = residual;
invalid.joints_m[0][0] = f32::NAN;
assert!(invalid.bounded(0.20).is_none());
}
#[cfg(feature = "learned-cpu")]
#[test]
fn burn_gru_executes_and_roundtrips_verified_record_on_cpu() {
use burn_core::tensor::Tensor;
use burn_ndarray::{NdArray, NdArrayDevice};
use wifi_densepose_physics::learned::model::{
ResidualGru, HIDDEN_WIDTH, HISTORY_FRAMES, JOINT_RESIDUAL_WIDTH,
};
type Backend = NdArray<f32>;
let device = NdArrayDevice::default();
let model = ResidualGru::<Backend>::init(&device);
assert!(burn_core::module::Module::num_params(&model) > HIDDEN_WIDTH);
let bytes = model.into_artifact_bytes().unwrap();
let hash = *blake3::hash(&bytes).as_bytes();
let artifact = LearnedArtifact::verified(
bytes.clone(),
hash,
LearnedArtifactManifest::adr323("cpu-roundtrip".into()),
SignatureVerification::accepted("test-release-key".into(), [5; 32]).unwrap(),
bytes.len(),
)
.unwrap();
let loaded = ResidualGru::<Backend>::from_verified_artifact(&artifact, &device).unwrap();
let input = Tensor::<Backend, 3>::zeros([1, HISTORY_FRAMES, FEATURE_WIDTH], &device);
let output = loaded.forward(input);
assert_eq!(
output.joint_residual.shape().dims(),
[1, JOINT_RESIDUAL_WIDTH]
);
assert_eq!(
output.residual_log_variance.shape().dims(),
[1, JOINT_RESIDUAL_WIDTH]
);
assert_eq!(output.contact_probability.shape().dims(), [1, 2]);
assert_eq!(output.abstention_probability.shape().dims(), [1, 1]);
let runtime =
wifi_densepose_physics::learned::runtime::CpuResidualRuntime::activate(&artifact).unwrap();
assert_eq!(runtime.artifact_hash(), hash);
let prediction = runtime
.predict(&vec![0.0; 20 * FEATURE_WIDTH], 0.20)
.unwrap();
assert!(prediction
.joints_m
.iter()
.flatten()
.all(|value| value.is_finite() && value.abs() <= 0.20));
assert!(runtime.predict(&[0.0; 3], 0.20).is_none());
}

View File

@@ -0,0 +1,30 @@
#![allow(missing_docs)]
mod common;
use common::observation;
use wifi_densepose_core::AbstentionReason;
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
#[test]
fn rejects_non_psd_covariance() {
let mut raw = observation(1);
raw.joints[0].covariance_m2.xy = 1.0;
raw.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
assert_eq!(
engine.process(&raw, raw.timestamp_ns).reason,
Some(AbstentionReason::InvalidCovariance)
);
}
#[test]
fn rejects_nan() {
let mut raw = observation(1);
raw.joints[0].position_m[0] = f32::NAN;
raw.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
assert_eq!(
engine.process(&raw, raw.timestamp_ns).reason,
Some(AbstentionReason::InvalidNumber)
);
}

View File

@@ -0,0 +1,96 @@
#![allow(missing_docs)]
mod common;
use proptest::prelude::*;
use wifi_densepose_core::PhysicsMode;
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
proptest! {
#[test]
fn finite_input_never_increases_confidence(dx in -0.05f32..0.05, dz in -0.05f32..0.05) {
let mut raw = common::observation(1);
raw.joints[15].position_m[0] += dx;
raw.joints[15].position_m[2] += dz;
raw.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig { mode: PhysicsMode::ShadowCorrect, ..PhysicsConfig::default() }).unwrap();
let result = engine.process(&raw, raw.timestamp_ns);
prop_assert!(result.effective_confidence.get() <= raw.observer_confidence.get());
if let Some(joints) = result.refined_joints_m {
prop_assert!(joints.iter().flatten().all(|value| value.is_finite()));
prop_assert!(result.intervention.max_joint_correction_m <= engine.config().max_joint_correction_m);
}
}
}
#[test]
fn translating_pose_and_floor_preserves_audit_residuals() {
let base = common::observation(1);
let mut translated = base.clone();
let delta = [3.0, -2.0, 0.75];
for joint in &mut translated.joints {
for (coordinate, offset) in joint.position_m.iter_mut().zip(delta) {
*coordinate += offset;
}
}
let floor = translated.floor_plane.as_mut().unwrap();
floor.offset_m -= floor
.normal
.iter()
.zip(delta)
.map(|(a, b)| a * b)
.sum::<f32>();
translated.track_id.0 = "local:translated".into();
translated.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
let first = engine.process(&base, base.timestamp_ns);
let second = engine.process(&translated, translated.timestamp_ns);
assert!((first.residuals.bone_m - second.residuals.bone_m).abs() < 1.0e-5);
assert!(
(first.residuals.floor_penetration_m - second.residuals.floor_penetration_m).abs() < 1.0e-5
);
}
#[test]
fn mirroring_symmetric_input_preserves_scalar_assessment() {
let raw = common::observation(1);
let mut mirrored = raw.clone();
for joint in &mut mirrored.joints {
joint.position_m[0] = -joint.position_m[0];
}
mirrored.track_id.0 = "local:mirror".into();
mirrored.seal();
let mut engine = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
let left = engine.process(&raw, raw.timestamp_ns);
let right = engine.process(&mirrored, mirrored.timestamp_ns);
assert!((left.residuals.normalized_total - right.residuals.normalized_total).abs() < 1.0e-5);
}
#[test]
fn independent_track_order_does_not_change_results() {
let a = common::observation(1);
let mut b = a.clone();
b.track_id.0 = "local:8".into();
b.joints[9].position_m[0] += 0.03;
b.seal();
let mut forward = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
let forward_a = forward.process(&a, a.timestamp_ns);
let forward_b = forward.process(&b, b.timestamp_ns);
let mut reverse = PhysicsEngine::new(PhysicsConfig::default()).unwrap();
let reverse_b = reverse.process(&b, b.timestamp_ns);
let reverse_a = reverse.process(&a, a.timestamp_ns);
assert_semantically_equal_ignoring_wall_clock(forward_a, reverse_a);
assert_semantically_equal_ignoring_wall_clock(forward_b, reverse_b);
}
fn assert_semantically_equal_ignoring_wall_clock(
mut left: wifi_densepose_core::PoseRefinementV1,
mut right: wifi_densepose_core::PoseRefinementV1,
) {
left.intervention.elapsed_us = 0;
right.intervention.elapsed_us = 0;
left.seal();
right.seal();
assert_eq!(left, right);
}

View File

@@ -0,0 +1,38 @@
#![allow(missing_docs)]
mod common;
use common::observation;
use std::path::PathBuf;
use wifi_densepose_physics::{PhysicsConfig, PhysicsEngine};
#[test]
fn checked_in_schemas_parse_and_cover_serialized_contract_fields() {
let raw = observation(1);
let result = PhysicsEngine::new(PhysicsConfig::default())
.unwrap()
.process(&raw, raw.timestamp_ns);
assert_schema_required_fields("pose-observation-v2.schema.json", &raw);
assert_schema_required_fields("pose-refinement-v1.schema.json", &result);
}
fn assert_schema_required_fields(name: &str, value: &impl serde::Serialize) {
let schema_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../docs/schemas")
.join(name);
let schema: serde_json::Value =
serde_json::from_slice(&std::fs::read(schema_path).unwrap()).unwrap();
assert_eq!(
schema["$schema"],
"https://json-schema.org/draft/2020-12/schema"
);
assert_eq!(schema["additionalProperties"], false);
let serialized = serde_json::to_value(value).unwrap();
for required in schema["required"].as_array().unwrap() {
let field = required.as_str().unwrap();
assert!(
serialized.get(field).is_some(),
"schema requires missing field {field}"
);
}
}

View File

@@ -46,6 +46,8 @@ clap = { workspace = true }
# from `lib.rs` so the Python `[aether]` wheel can bind it without this server's
# Axum/tokio/worldgraph/ruvector tree.
wifi-densepose-aether = { version = "0.3.0", path = "../wifi-densepose-aether" }
wifi-densepose-core = { workspace = true, features = ["serde"] }
wifi-densepose-physics = { workspace = true }
# Multi-BSSID WiFi scanning pipeline (ADR-022 Phase 3)
wifi-densepose-wifiscan = { version = "0.3.0", path = "../wifi-densepose-wifiscan" }

View File

@@ -23,6 +23,8 @@ pub mod matter;
pub mod model_format;
pub mod mqtt;
pub mod path_safety;
/// ADR-323: fail-closed pose physics integration and raw/refined view selection.
pub mod pose_physics;
/// ADR-295: canonical source-provenance state machine (synthetic can never
/// present as live).
pub mod provenance;

View File

@@ -22,6 +22,7 @@ mod qualcomm_csi;
mod realtek_radar;
mod path_safety;
pub mod pose;
pub mod pose_physics;
mod rvf_container;
// ADR-186 (TRAIN-RECONNECT): the in-server training pipeline was written but
// never declared as a module, so it was orphaned / uncompiled. Declaring it
@@ -1382,6 +1383,80 @@ struct AppStateInner {
/// Held behind its own `Arc<RwLock<_>>` so the additive field router can
/// take it as state without re-locking `AppStateInner`.
field_surface: rufield_surface::FieldState,
/// Canonical ADR-323 engine and latest additive publication. Existing
/// image-space renderer poses are never silently inserted here.
pose_physics: pose_physics::PosePhysicsRuntime,
}
#[cfg(test)]
mod adr323_pose_physics_http_tests {
use super::*;
use axum::{body::Body, http::Request};
use tower::ServiceExt;
fn app() -> Router {
Router::new()
.route("/api/v1/pose/current", get(pose_current))
.route("/api/v1/pose/physics/metrics", get(pose_physics_metrics))
.with_state(Arc::new(RwLock::new(AppStateInner::minimal())))
}
#[tokio::test]
async fn raw_view_remains_backward_compatible() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/v1/pose/current?view=raw")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), 65_536)
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(body.get("persons").is_some());
assert!(body.get("physics").is_none());
}
#[tokio::test]
async fn refined_view_is_typed_conflict_when_no_selected_pose_exists() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/v1/pose/current?view=refined")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CONFLICT);
let body = axum::body::to_bytes(response.into_body(), 65_536)
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(body["code"], "pose_refined_unavailable");
}
#[tokio::test]
async fn physics_metrics_use_prometheus_content_type() {
let response = app()
.oneshot(
Request::builder()
.uri("/api/v1/pose/physics/metrics")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers()[axum::http::header::CONTENT_TYPE],
"text/plain; version=0.0.4"
);
}
}
/// If no ESP32 frame arrives within this duration, source reverts to offline.
@@ -1551,6 +1626,10 @@ impl AppStateInner {
dedup_factor: 3.0,
data_dir: std::path::PathBuf::from("data"),
field_surface: Arc::new(RwLock::new(rufield_surface::FieldSurface::from_env())),
pose_physics: pose_physics::PosePhysicsRuntime::new(
wifi_densepose_physics::PhysicsConfig::default(),
)
.expect("default pose physics configuration is valid"),
}
}
}
@@ -3001,6 +3080,7 @@ async fn windows_wifi_task(state: SharedState, tick_ms: u64) {
}
// #1050: attach real signal_field-peak positions to each person.
attach_field_positions(&mut update);
assess_legacy_image_pose(&mut s, &update);
if let Ok(json) = serde_json::to_string(&update) {
let _ = s.tx.send(json);
@@ -3159,6 +3239,7 @@ async fn windows_wifi_fallback_tick(state: &SharedState, seq: u32) {
}
// #1050: attach real signal_field-peak positions to each person.
attach_field_positions(&mut update);
assess_legacy_image_pose(&mut s, &update);
if let Ok(json) = serde_json::to_string(&update) {
let _ = s.tx.send(json);
@@ -3572,9 +3653,18 @@ async fn handle_ws_pose_client(mut socket: WebSocket, state: SharedState) {
// Determine pose estimation mode for the UI indicator.
// "model_inference" — a trained RVF model is loaded.
// "signal_derived" — keypoints estimated from raw CSI features.
let model_loaded = {
let (model_loaded, physics_assessment) = {
let s = state.read().await;
s.model_loaded
let physics = s.pose_physics.latest().and_then(|(raw, result)| {
(raw.sequence == sensing.tick).then(|| {
serde_json::json!({
"schema": "pose-refinement-v1",
"raw_observation_hash": raw.canonical_hash,
"assessment": result,
})
})
});
(s.model_loaded, physics)
};
let pose_source = if model_loaded {
"model_inference"
@@ -3627,6 +3717,7 @@ async fn handle_ws_pose_client(mut socket: WebSocket, state: SharedState) {
"type": "pose_data",
"zone_id": "zone_1",
"timestamp": sensing.timestamp,
"physics": physics_assessment,
"payload": {
"pose": {
"persons": persons,
@@ -4630,6 +4721,110 @@ fn attach_field_positions(update: &mut SensingUpdate) {
}
}
/// Feed legacy renderer coordinates into the canonical audit boundary without
/// promoting them to metric 3D or calibrated evidence.
fn assess_legacy_image_pose(state: &mut AppStateInner, update: &SensingUpdate) {
use wifi_densepose_core::{
CalibrationId, Coco17Joint, JointObservation, JointVisibility, ModelRef,
PoseDimensionality, PoseObservationV2, PoseTrustState, Probability, SourceProvenance,
SpatialFrameRef, SymmetricCovariance3, TrackId,
};
if !update.timestamp.is_finite() || update.timestamp < 0.0 {
return;
}
let timestamp_ns = (update.timestamp * 1_000_000_000.0) as u64;
let Some(persons) = update.persons.as_ref() else {
return;
};
for person in persons {
if person.keypoints.len() != Coco17Joint::ALL.len()
|| !person.confidence.is_finite()
{
continue;
}
let Some(joints) = person
.keypoints
.iter()
.zip(Coco17Joint::ALL)
.map(|(keypoint, kind)| {
let position = [keypoint.x as f32, keypoint.y as f32, 0.0];
if !position.iter().all(|coordinate| coordinate.is_finite())
|| !keypoint.confidence.is_finite()
{
return None;
}
let confidence =
Probability::new((keypoint.confidence as f32).clamp(0.0, 1.0)).ok()?;
Some(JointObservation {
kind,
position_m: position,
covariance_m2: SymmetricCovariance3 {
xx: 0.0,
xy: 0.0,
xz: 0.0,
yy: 0.0,
yz: 0.0,
zz: 0.0,
},
confidence,
visibility: if confidence.get() > 0.0 {
JointVisibility::Visible
} else {
JointVisibility::Unknown
},
})
})
.collect::<Option<Vec<_>>>()
.and_then(|joints| joints.try_into().ok())
else {
continue;
};
let observer_confidence =
match Probability::new((person.confidence as f32).clamp(0.0, 1.0)) {
Ok(confidence) => confidence,
Err(_) => continue,
};
let mut raw = PoseObservationV2 {
schema_version: 2,
timestamp_ns,
sensor_epoch: 1,
sequence: update.tick,
track_id: TrackId(format!("local:{}", person.id)),
frame: SpatialFrameRef {
name: "legacy-renderer-image".into(),
version: 1,
metric: false,
right_handed: false,
z_up: false,
},
calibration_id: CalibrationId("uncalibrated-image".into()),
floor_plane: None,
model: ModelRef {
id: if state.model_loaded {
"sensing-server-model-artifact-unknown".into()
} else {
"sensing-server-signal-derived".into()
},
artifact_hash: [0; 32],
},
source: SourceProvenance {
sensor_id: "sensing-server-local".into(),
authenticated: false,
replay_protected: false,
},
trust_state: PoseTrustState::Degraded,
dimensionality: PoseDimensionality::Image2d,
uncertainty_calibrated: false,
joints,
observer_confidence,
canonical_hash: [0; 32],
};
raw.seal();
let _ = state.pose_physics.process(&raw, timestamp_ns);
}
}
fn derive_pose_from_sensing(update: &SensingUpdate) -> Vec<PersonDetection> {
let cls = &update.classification;
if !cls.presence {
@@ -4845,7 +5040,16 @@ async fn api_info(State(state): State<SharedState>) -> Json<serde_json::Value> {
}))
}
async fn pose_current(State(state): State<SharedState>) -> Json<serde_json::Value> {
#[derive(Debug, Default, Deserialize)]
struct PoseCurrentQuery {
#[serde(default)]
view: pose_physics::PoseView,
}
async fn pose_current(
State(state): State<SharedState>,
Query(query): Query<PoseCurrentQuery>,
) -> impl IntoResponse {
let s = state.read().await;
let persons = match &s.latest_update {
Some(update) => update
@@ -4854,12 +5058,69 @@ async fn pose_current(State(state): State<SharedState>) -> Json<serde_json::Valu
.unwrap_or_else(|| derive_pose_from_sensing(update)),
None => vec![],
};
Json(serde_json::json!({
let legacy = serde_json::json!({
"timestamp": chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
"persons": persons,
"total_persons": persons.len(),
"source": s.effective_source(),
}))
});
match query.view {
pose_physics::PoseView::Raw => (StatusCode::OK, Json(legacy)),
pose_physics::PoseView::Both => {
if let Some((raw, physics)) = s.pose_physics.latest() {
let mut body = legacy;
body["raw"] = serde_json::to_value(raw).unwrap_or(serde_json::Value::Null);
body["physics"] =
serde_json::to_value(physics).unwrap_or(serde_json::Value::Null);
(StatusCode::OK, Json(body))
} else {
(
StatusCode::CONFLICT,
Json(serde_json::json!({
"code": "pose_physics_unavailable",
"detail": "no canonical pose observation has been assessed"
})),
)
}
}
pose_physics::PoseView::Refined => {
let Some((raw, physics)) = s.pose_physics.latest() else {
return (
StatusCode::CONFLICT,
Json(serde_json::json!({
"code": "pose_refined_unavailable",
"detail": "no canonical pose observation has been assessed"
})),
);
};
match pose_physics::PosePhysicsRuntime::select(query.view, raw, physics) {
Ok(pose_physics::SelectedPose::Refined(joints)) => (
StatusCode::OK,
Json(serde_json::json!({
"schema_version": 1,
"raw_observation_hash": raw.canonical_hash,
"joints_m": joints,
"physics": physics,
})),
),
Ok(_) => unreachable!("refined view returns only refined data"),
Err(error) => (
StatusCode::CONFLICT,
Json(serde_json::to_value(error).unwrap_or_else(|_| {
serde_json::json!({"code": "pose_refined_unavailable"})
})),
),
}
}
}
}
async fn pose_physics_metrics(State(state): State<SharedState>) -> impl IntoResponse {
let metrics = state.read().await.pose_physics.metrics_text();
(
[(axum::http::header::CONTENT_TYPE, "text/plain; version=0.0.4")],
metrics,
)
}
async fn pose_stats(State(state): State<SharedState>) -> Json<serde_json::Value> {
@@ -6256,6 +6517,7 @@ async fn udp_receiver_task(
}
// #1050: attach real signal_field-peak positions to each person.
attach_field_positions(&mut update);
assess_legacy_image_pose(&mut s, &update);
if let Ok(json) = serde_json::to_string(&update) {
let _ = s.tx.send(json);
@@ -6708,6 +6970,7 @@ async fn udp_receiver_task(
}
// #1050: attach real signal_field-peak positions to each person.
attach_field_positions(&mut update);
assess_legacy_image_pose(&mut s, &update);
if let Ok(json) = serde_json::to_string(&update) {
let _ = s.tx.send(json);
@@ -6968,6 +7231,7 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
}
// #1050: attach real signal_field-peak positions to each person.
attach_field_positions(&mut update);
assess_legacy_image_pose(&mut s, &update);
if update.classification.presence {
s.total_detections += 1;
@@ -8313,6 +8577,10 @@ async fn main() {
dedup_factor: runtime_config.dedup_factor,
data_dir: data_dir.clone(),
field_surface: field_surface.clone(),
pose_physics: pose_physics::PosePhysicsRuntime::new(
wifi_densepose_physics::PhysicsConfig::default(),
)
.expect("default pose physics configuration is valid"),
}));
// Start background tasks from the resolved plan (issue #1004).
@@ -8531,6 +8799,7 @@ async fn main() {
.route("/api/v1/model/sona/activate", post(sona_activate))
// Pose endpoints (WiFi-derived)
.route("/api/v1/pose/current", get(pose_current))
.route("/api/v1/pose/physics/metrics", get(pose_physics_metrics))
.route("/api/v1/pose/stats", get(pose_stats))
.route("/api/v1/pose/zones/summary", get(pose_zones_summary))
.route("/api/v1/pose/activities", get(pose_activities))
@@ -9566,6 +9835,29 @@ mod observatory_persons_field_position_tests {
assert!((pj["motion_score"].as_f64().unwrap() - 63.3).abs() < 1e-6);
}
#[test]
fn legacy_live_pose_is_audited_as_uncalibrated_image_data() {
let mut update = base_update(field_with_peak(10, 10), true, 20.0);
update.persons = Some(derive_pose_from_sensing(&update));
let mut state = AppStateInner::minimal();
assess_legacy_image_pose(&mut state, &update);
let (raw, result) = state
.pose_physics
.latest()
.expect("legacy image pose should reach canonical audit");
assert_eq!(raw.dimensionality, wifi_densepose_core::PoseDimensionality::Image2d);
assert!(!raw.uncertainty_calibrated);
assert!(!raw.source.authenticated);
assert_eq!(
result.disposition,
wifi_densepose_core::RefinementDisposition::Audited2d
);
assert!(!result.selected);
assert!(result.refined_joints_m.is_none());
}
#[test]
fn pose_is_real_when_posture_present_and_absent_otherwise() {
// No aggregate posture estimate → pose is None (never fabricated).

View File

@@ -0,0 +1,336 @@
//! Fail-closed sensing-server integration for ADR-323.
//!
//! This adapter deliberately accepts only the canonical contract. Existing
//! renderer poses are image/pixel-space and must not be silently promoted to
//! metric 3D observations.
use std::collections::BTreeMap;
use wifi_densepose_core::{
PhysicsMode, PoseObservationV2, PoseRefinementV1, RefinementDisposition,
};
use wifi_densepose_physics::{
metrics::PhysicsMetrics, CorrectionAuthorization, PhysicsConfig, PhysicsEngine, PhysicsError,
VerifiedFrameContext,
};
/// Operator-requested API representation.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PoseView {
/// Immutable observer output only (migration default).
#[default]
Raw,
/// Observer output plus additive physics assessment.
Both,
/// A selected corrected pose; unavailable is a typed conflict.
Refined,
}
/// Borrowed publication selected without relabeling raw data.
#[derive(Debug)]
pub enum SelectedPose<'a> {
Raw(&'a PoseObservationV2),
Both {
raw: &'a PoseObservationV2,
physics: &'a PoseRefinementV1,
},
Refined(&'a [[f32; 3]; 17]),
}
/// Typed body for HTTP 409 when `view=refined` is unavailable.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
pub struct RefinedUnavailable {
pub code: &'static str,
pub disposition: RefinementDisposition,
}
/// Result of scheduling refinement work while raw publication continues.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EnqueueOutcome {
/// First pending frame for this track.
Queued,
/// A pending frame was discarded and the newest frame was retained.
ReplacedIntermediate,
/// A new track could not be scheduled within the configured cardinality cap.
RejectedCapacity,
}
/// Process-local engine wrapper. Control-plane authentication is enforced by
/// the caller before [`Self::set_mode`] is invoked.
pub struct PosePhysicsRuntime {
engine: PhysicsEngine,
metrics: PhysicsMetrics,
latest: Option<(PoseObservationV2, PoseRefinementV1)>,
pending: BTreeMap<(u64, String), PoseObservationV2>,
max_pending_tracks: usize,
}
impl PosePhysicsRuntime {
/// Create the engine with validated, operator-owned limits.
pub fn new(config: PhysicsConfig) -> Result<Self, PhysicsError> {
let max_pending_tracks = config.max_tracks;
Ok(Self {
engine: PhysicsEngine::new(config)?,
metrics: PhysicsMetrics::default(),
latest: None,
pending: BTreeMap::new(),
max_pending_tracks,
})
}
/// Retain only the newest pending frame per track. This queue covers
/// refinement work only; callers must publish every immutable raw frame
/// independently before scheduling it here.
pub fn enqueue_latest(&mut self, raw: PoseObservationV2) -> EnqueueOutcome {
let key = (raw.sensor_epoch, raw.track_id.0.clone());
if let Some(current) = self.pending.get_mut(&key) {
if (raw.timestamp_ns, raw.sequence) > (current.timestamp_ns, current.sequence) {
*current = raw;
}
self.metrics.dropped_intermediate();
return EnqueueOutcome::ReplacedIntermediate;
}
if self.pending.len() >= self.max_pending_tracks {
return EnqueueOutcome::RejectedCapacity;
}
self.pending.insert(key, raw);
EnqueueOutcome::Queued
}
/// Drain the bounded queue in deterministic track order.
#[must_use]
pub fn process_pending(&mut self, now_ns: u64) -> Vec<PoseRefinementV1> {
let pending = core::mem::take(&mut self.pending);
pending
.into_values()
.map(|raw| self.process(&raw, now_ns))
.collect()
}
/// Assess exactly one canonical observation.
#[must_use]
pub fn process(&mut self, raw: &PoseObservationV2, now_ns: u64) -> PoseRefinementV1 {
let result = self.engine.process(raw, now_ns);
self.record(raw, &result);
result
}
/// Process a frame whose signed envelope and replay window were verified by
/// the serving data plane. Corrected selection is impossible without this receipt.
#[must_use]
pub fn process_verified(
&mut self,
raw: &PoseObservationV2,
now_ns: u64,
verification: &VerifiedFrameContext,
) -> PoseRefinementV1 {
let result = self.engine.process_verified(raw, now_ns, verification);
self.record(raw, &result);
result
}
/// Activate release-gate evidence previously verified by the signed local
/// control plane.
pub fn authorize_correction(
&mut self,
authorization: CorrectionAuthorization,
) -> Result<(), PhysicsError> {
self.engine.authorize_correction(authorization)
}
/// Active configuration hash used to bind signed evidence receipts.
#[must_use]
pub const fn config_hash(&self) -> [u8; 32] {
self.engine.config_hash()
}
/// Apply a previously authenticated and witnessed mode transition.
pub fn set_mode(&mut self, mode: PhysicsMode) -> Result<(), PhysicsError> {
self.engine.set_mode(mode)?;
if mode == PhysicsMode::Off {
self.latest = None;
self.pending.clear();
}
Ok(())
}
/// Most recent immutable raw/result pair, if a canonical frame was processed.
#[must_use]
pub const fn latest(&self) -> Option<&(PoseObservationV2, PoseRefinementV1)> {
self.latest.as_ref()
}
/// Privacy-safe Prometheus exposition with fixed-cardinality labels only.
#[must_use]
pub fn metrics_text(&self) -> String {
self.metrics.encode_prometheus()
}
fn record(&mut self, raw: &PoseObservationV2, result: &PoseRefinementV1) {
self.metrics.observe(result, raw.observer_confidence.get());
self.latest = Some((raw.clone(), result.clone()));
}
/// Select an additive API view. Refined never silently falls back to raw.
pub fn select<'a>(
view: PoseView,
raw: &'a PoseObservationV2,
physics: &'a PoseRefinementV1,
) -> Result<SelectedPose<'a>, RefinedUnavailable> {
match view {
PoseView::Raw => Ok(SelectedPose::Raw(raw)),
PoseView::Both => Ok(SelectedPose::Both { raw, physics }),
PoseView::Refined => physics
.refined_joints_m
.as_ref()
.filter(|_| physics.selected)
.map(SelectedPose::Refined)
.ok_or(RefinedUnavailable {
code: "pose_refined_unavailable",
disposition: physics.disposition,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use wifi_densepose_core::{
AbstentionReason, CalibrationId, Coco17Joint, ConstraintResiduals, ContactHypothesis,
InterventionSummary, JointObservation, JointVisibility, ModelRef, PhysicsProvenance,
PoseDimensionality, PoseTrustState, Probability, SourceProvenance, SpatialFrameRef,
SymmetricCovariance3, TrackId,
};
fn pair() -> (PoseObservationV2, PoseRefinementV1) {
let joints = Coco17Joint::ALL.map(|kind| JointObservation {
kind,
position_m: [0.0; 3],
covariance_m2: SymmetricCovariance3 {
xx: 0.0,
xy: 0.0,
xz: 0.0,
yy: 0.0,
yz: 0.0,
zz: 0.0,
},
confidence: Probability::ZERO,
visibility: JointVisibility::Unknown,
});
let mut raw = PoseObservationV2 {
schema_version: 2,
timestamp_ns: 1,
sensor_epoch: 1,
sequence: 1,
track_id: TrackId("local:1".into()),
frame: SpatialFrameRef {
name: "image".into(),
version: 1,
metric: false,
right_handed: false,
z_up: false,
},
calibration_id: CalibrationId("none".into()),
floor_plane: None,
model: ModelRef {
id: "test".into(),
artifact_hash: [0; 32],
},
source: SourceProvenance {
sensor_id: "test".into(),
authenticated: false,
replay_protected: false,
},
trust_state: PoseTrustState::Degraded,
dimensionality: PoseDimensionality::Image2d,
uncertainty_calibrated: false,
joints,
observer_confidence: Probability::ZERO,
canonical_hash: [0; 32],
};
raw.seal();
let physics = PoseRefinementV1 {
schema_version: 1,
raw_observation_hash: raw.canonical_hash,
mode: PhysicsMode::Audit,
disposition: RefinementDisposition::Audited2d,
selected: false,
refined_joints_m: None,
physics_confidence: Probability::ONE,
effective_confidence: Probability::ZERO,
intervention: InterventionSummary::default(),
residuals: ConstraintResiduals::default(),
refined_residuals: None,
dynamics: None,
contact_hypotheses: [ContactHypothesis::default(); 2],
provenance: PhysicsProvenance {
engine: "test".into(),
engine_version: "1".into(),
config_hash: [0; 32],
rf_model_hash: [0; 32],
calibration_id: "none".into(),
learned_artifact_hash: None,
},
reason: Some(AbstentionReason::UncertaintyUncalibrated),
canonical_hash: [0; 32],
};
(raw, physics)
}
#[test]
fn refined_view_never_silently_falls_back() {
let (raw, physics) = pair();
let error = PosePhysicsRuntime::select(PoseView::Refined, &raw, &physics).unwrap_err();
assert_eq!(error.code, "pose_refined_unavailable");
}
#[test]
fn processing_publishes_latest_pair_and_privacy_safe_metrics() {
let (raw, _) = pair();
let mut runtime = PosePhysicsRuntime::new(PhysicsConfig::default()).unwrap();
let result = runtime.process(&raw, raw.timestamp_ns);
let (published_raw, published_result) = runtime.latest().unwrap();
assert_eq!(published_raw.canonical_hash, raw.canonical_hash);
assert_eq!(published_result.canonical_hash, result.canonical_hash);
let metrics = runtime.metrics_text();
assert!(metrics.contains("ruview_pose_physics_frames_total"));
assert!(metrics.contains("ruview_pose_physics_confidence_delta"));
assert!(!metrics.contains(&raw.track_id.0));
assert!(!metrics.contains(&raw.source.sensor_id));
}
#[test]
fn backpressure_retains_newest_frame_per_track_with_a_hard_cap() {
let (raw, _) = pair();
let mut config = PhysicsConfig::default();
config.max_tracks = 1;
let mut runtime = PosePhysicsRuntime::new(config).unwrap();
assert_eq!(runtime.enqueue_latest(raw.clone()), EnqueueOutcome::Queued);
let mut newer = raw.clone();
newer.sequence = 2;
newer.timestamp_ns = 2;
newer.seal();
assert_eq!(
runtime.enqueue_latest(newer.clone()),
EnqueueOutcome::ReplacedIntermediate
);
let mut second_track = raw;
second_track.track_id = TrackId("local:2".into());
second_track.seal();
assert_eq!(
runtime.enqueue_latest(second_track),
EnqueueOutcome::RejectedCapacity
);
let results = runtime.process_pending(newer.timestamp_ns);
assert_eq!(results.len(), 1);
assert_eq!(results[0].raw_observation_hash, newer.canonical_hash);
assert!(runtime
.metrics_text()
.contains("ruview_pose_physics_dropped_intermediate_total 1"));
}
}