diff --git a/docs/adr/ADR-323-native-rust-physics-constrained-pose-refinement.md b/docs/adr/ADR-323-native-rust-physics-constrained-pose-refinement.md new file mode 100644 index 00000000..dbc014e1 --- /dev/null +++ b/docs/adr/ADR-323-native-rust-physics-constrained-pose-refinement.md @@ -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) diff --git a/docs/adr/README.md b/docs/adr/README.md index 30b240f6..2b7a6ff1 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -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 | --- diff --git a/docs/benchmarks/physics-pose-refinement.md b/docs/benchmarks/physics-pose-refinement.md new file mode 100644 index 00000000..fa3bf8f6 --- /dev/null +++ b/docs/benchmarks/physics-pose-refinement.md @@ -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 +bash scripts/replay-pose-physics-golden.sh +``` diff --git a/docs/schemas/pose-observation-v2.schema.json b/docs/schemas/pose-observation-v2.schema.json new file mode 100644 index 00000000..dc470319 --- /dev/null +++ b/docs/schemas/pose-observation-v2.schema.json @@ -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"] } } } + } +} diff --git a/docs/schemas/pose-refinement-v1.schema.json b/docs/schemas/pose-refinement-v1.schema.json new file mode 100644 index 00000000..99c94630 --- /dev/null +++ b/docs/schemas/pose-refinement-v1.schema.json @@ -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" }] } } } + } +} diff --git a/scripts/pose-physics/testdata/golden-results.jsonl b/scripts/pose-physics/testdata/golden-results.jsonl new file mode 100644 index 00000000..e85000f5 --- /dev/null +++ b/scripts/pose-physics/testdata/golden-results.jsonl @@ -0,0 +1 @@ +{"ankle_penetration_m":0.04,"evidence":"SYNTHETIC/L0","expected_raw_hash":"98caf45c249d584d52ede84e2d76bd8c4bf225d426a4ef1651a5ddb178258b88","expected_result_hash":"8ef2ded432c938055a5ebc86eadff811216884093853f5c740c7aff7708b9bef","sequence":1} diff --git a/scripts/pose-physics/testdata/leaky-split-manifest.json b/scripts/pose-physics/testdata/leaky-split-manifest.json new file mode 100644 index 00000000..36ff8a4c --- /dev/null +++ b/scripts/pose-physics/testdata/leaky-split-manifest.json @@ -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" + } + ] +} diff --git a/scripts/pose-physics/testdata/strict-split-manifest.json b/scripts/pose-physics/testdata/strict-split-manifest.json new file mode 100644 index 00000000..5b08ea43 --- /dev/null +++ b/scripts/pose-physics/testdata/strict-split-manifest.json @@ -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" } + ] +} diff --git a/scripts/pose-physics/verify-feature-boundary.mjs b/scripts/pose-physics/verify-feature-boundary.mjs new file mode 100644 index 00000000..9dc9f1f7 --- /dev/null +++ b/scripts/pose-physics/verify-feature-boundary.mjs @@ -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"); diff --git a/scripts/pose-physics/verify-golden.mjs b/scripts/pose-physics/verify-golden.mjs new file mode 100644 index 00000000..b769da30 --- /dev/null +++ b/scripts/pose-physics/verify-golden.mjs @@ -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 "); +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); diff --git a/scripts/pose-physics/verify-harness-tests.mjs b/scripts/pose-physics/verify-harness-tests.mjs new file mode 100644 index 00000000..aa40626c --- /dev/null +++ b/scripts/pose-physics/verify-harness-tests.mjs @@ -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"); diff --git a/scripts/pose-physics/verify-splits.mjs b/scripts/pose-physics/verify-splits.mjs new file mode 100644 index 00000000..f3b0f5d3 --- /dev/null +++ b/scripts/pose-physics/verify-splits.mjs @@ -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 "); +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 })); diff --git a/scripts/replay-pose-physics-golden.sh b/scripts/replay-pose-physics-golden.sh new file mode 100644 index 00000000..e7f3fedb --- /dev/null +++ b/scripts/replay-pose-physics-golden.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +node "$(dirname "$0")/pose-physics/verify-golden.mjs" "$@" diff --git a/scripts/verify-pose-physics-splits.sh b/scripts/verify-pose-physics-splits.sh new file mode 100644 index 00000000..a4e64c81 --- /dev/null +++ b/scripts/verify-pose-physics-splits.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +node "$(dirname "$0")/pose-physics/verify-splits.mjs" "$@" diff --git a/v2/Cargo.lock b/v2/Cargo.lock index d80053d0..a4cdbe25 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -38,6 +38,18 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -47,6 +59,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -115,7 +136,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0a8acea8c2f1c60f0a92a8cd26bf96ca97db56f10bbcab238bbe0cceba659ee" dependencies = [ - "nom", + "nom 7.1.3", "vte", ] @@ -179,7 +200,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -190,7 +211,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -241,6 +262,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading 0.8.9", +] + [[package]] name = "assert_cmd" version = "2.1.2" @@ -256,6 +286,18 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" version = "0.4.42" @@ -326,6 +368,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomic_float" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" + [[package]] name = "autocfg" version = "1.5.0" @@ -402,6 +450,21 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -478,7 +541,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec 0.9.1", ] [[package]] @@ -487,6 +559,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + [[package]] name = "bitflags" version = "1.3.2" @@ -584,6 +662,312 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "burn-autodiff" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b93a80e43bfab909399444b29ce3894ff51f7e879720bfc75b6007ae6a01c3d" +dependencies = [ + "burn-backend", + "burn-std", + "derive-new", + "hashbrown 0.16.1", + "log", + "num-traits", + "portable-atomic", + "spin 0.10.1", +] + +[[package]] +name = "burn-backend" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64491519ac0867f550fa9c726cd443e5db94608c629050986cf2614c8c5ad39f" +dependencies = [ + "burn-std", + "bytemuck", + "cubecl", + "derive-new", + "enumset", + "hashbrown 0.16.1", + "num-traits", + "portable-atomic-util", + "rand 0.10.2", + "rand_distr 0.6.0", + "serde", + "spin 0.10.1", + "thiserror 2.0.18", +] + +[[package]] +name = "burn-core" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bde1e3b8e7e39b0aa71cd5905207ceea5c3478bec47bdb64acf72dcd0d6f978" +dependencies = [ + "ahash", + "bincode 2.0.1", + "burn-dataset", + "burn-derive", + "burn-std", + "burn-tensor", + "data-encoding", + "derive-new", + "flate2", + "half", + "hashbrown 0.16.1", + "log", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rand 0.10.2", + "regex", + "rmp-serde", + "serde", + "serde_json", + "spin 0.10.1", + "uuid", +] + +[[package]] +name = "burn-cubecl" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26efbec96ca3f691593c966fa90f98ef8a72867f62627904920b619ba3a9e6b6" +dependencies = [ + "burn-backend", + "burn-cubecl-fusion", + "burn-fusion", + "burn-ir", + "burn-std", + "cubecl", + "cubek", + "derive-new", + "futures-lite", + "log", + "serde", + "text_placeholder", +] + +[[package]] +name = "burn-cubecl-fusion" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6993d15b8ed588f55626e835ca768c1c3c7aa7a79d59beff4c987c40fc82d1f4" +dependencies = [ + "burn-backend", + "burn-fusion", + "burn-ir", + "burn-std", + "cubecl", + "cubek", + "derive-new", + "hashbrown 0.16.1", + "serde", + "spin 0.10.1", +] + +[[package]] +name = "burn-cuda" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c66b49136fc11f59773054358f401fbec7fa0d69615f55ef22c29cf241c9b7" +dependencies = [ + "burn-backend", + "burn-cubecl", + "cubecl", +] + +[[package]] +name = "burn-dataset" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba469fba38357c8bb65458873ddfd82aa97534aec0b3bae93d4c44604fc63839" +dependencies = [ + "csv", + "derive-new", + "dirs 6.0.0", + "rand 0.10.2", + "rmp-serde", + "sanitize-filename", + "serde", + "serde_json", + "strum", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "burn-derive" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce7be43cdfc9903c43dbaf3f4821543e0497d79047d8fc9ee9084e448844446" +dependencies = [ + "derive-new", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "burn-flex" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0098bf6555c151517b4c98ca0e8ed1ab6a976e40f5f4e967c117b00eee21e192" +dependencies = [ + "aligned-vec", + "burn-backend", + "burn-ir", + "burn-std", + "bytemuck", + "gemm 0.19.0", + "half", + "libm", + "macerator", + "num-traits", + "rayon", +] + +[[package]] +name = "burn-fusion" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e58fcfd52a60cdf2f7da34fbebf0ed38f960fbd8675d384bacd5795cf2bdf2" +dependencies = [ + "burn-backend", + "burn-ir", + "burn-std", + "derive-new", + "hashbrown 0.16.1", + "log", + "serde", + "smallvec", + "spin 0.10.1", + "tracing", +] + +[[package]] +name = "burn-ir" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a81a55c32f35c2265d9e15ba2e796013d9780d5a49f9e21ea0ac4d29609dbf13" +dependencies = [ + "burn-backend", + "hashbrown 0.16.1", + "serde", +] + +[[package]] +name = "burn-ndarray" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dceb9692e292782bab7ad0259e8e8f5ee80ba2b0329a78f0ba589a26a9633dd6" +dependencies = [ + "atomic_float", + "burn-autodiff", + "burn-backend", + "burn-ir", + "burn-std", + "const-random", + "libm", + "macerator", + "matrixmultiply", + "ndarray 0.17.2", + "num-traits", + "paste", + "portable-atomic", + "portable-atomic-util", + "rand 0.10.2", +] + +[[package]] +name = "burn-nn" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e4acd90806fa8663610f1071f3a8992eb98637d2d0816ee3062b334f61af00" +dependencies = [ + "burn-core", + "num-traits", +] + +[[package]] +name = "burn-optim" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c5ea466ae0b85bf18f7656cdc152050dc8c581057fbbe33bbed267e22600228" +dependencies = [ + "burn-core", + "derive-new", + "hashbrown 0.16.1", + "log", + "num-traits", + "serde", +] + +[[package]] +name = "burn-std" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81c7eb60eb2c316854af5b214fd55f59fc7b0e25dfde7db671f09cc49f269048" +dependencies = [ + "bytemuck", + "bytes", + "cubecl", + "cubecl-common", + "cubecl-zspace", + "half", + "num-traits", + "serde", + "smallvec", + "spin 0.10.1", +] + +[[package]] +name = "burn-tensor" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223ed3804bb9436e401fc57b87e44c86267070b870813520ed9f706c80c81442" +dependencies = [ + "burn-backend", + "burn-std", + "colored 3.1.1", + "derive-new", + "num-traits", + "serde", +] + +[[package]] +name = "burn-train" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa65e2e569e571bb8dc9d37aa8fcf715f522f8159c6ee87f665c1961fee5259f" +dependencies = [ + "async-channel", + "burn-core", + "burn-flex", + "burn-optim", + "derive-new", + "log", + "rand 0.10.2", + "rstest", + "serde", + "thiserror 2.0.18", + "tracing-appender", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "burn-wgpu" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eb009254af15922cb37814f3b3b61043046193ba2fde97c3879a25fbd51a92e" +dependencies = [ + "burn-backend", + "burn-cubecl", + "cubecl", +] + [[package]] name = "bytecheck" version = "0.8.2" @@ -645,6 +1029,7 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ + "portable-atomic", "serde", ] @@ -921,6 +1306,17 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chacha20poly1305" version = "0.10.1" @@ -928,7 +1324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1041,6 +1437,17 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "cog-ha-matter" version = "0.3.1" @@ -1100,6 +1507,7 @@ dependencies = [ "tracing", "tracing-subscriber", "ureq 2.12.1", + "wifi-densepose-core", "wifi-densepose-train", ] @@ -1119,6 +1527,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -1202,6 +1619,26 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1220,6 +1657,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -1325,36 +1771,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd990d8a6304475bbad64534a0d418f5572f44d5f011437e6b9f1ee7d5c2570" +checksum = "5f8e1303ae2128891cb59691a74de4547dd208bc8511a2f287cca2b93bb3c728" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccabe4636007296721080e02d7dab46d4319638ec4e3f6f7402fcb46dc5122c6" +checksum = "58b2740a5936332028d9a1e8f29a199de2fd386e426d44da5fea70cf8e3f8e75" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7ed173c870c0aea202a9830880156905a028a88df076e35ce383a8acbf90a7" +checksum = "37fd128d3629fb105e433bda09744c5a2959cd1da04617a455c4cc17dff1ebef" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "800cc586df98b12c502e76707c96565e40629a5322eaa15aaa34ba05f5721e31" +checksum = "24a88f6d5a6cf6fcbc6386415d48948094721dfa4585d6615938b11ca938f20a" dependencies = [ "serde", "serde_derive", @@ -1362,9 +1808,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae93f863f9094ae34d2567f9edb0ae2c41d35228b286598354dd78b198868ebd" +checksum = "daa4a357d030bdd586d8fe3da56b394f7f6b6ded506e59f945e7b32b1e126b71" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1380,7 +1826,7 @@ dependencies = [ "log", "pulley-interpreter", "regalloc2", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "smallvec", "target-lexicon 0.13.5", @@ -1389,9 +1835,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c505162bcf77dcb859905b3eac56a1917fc3cf326424fb06e7732031e3a8ae" +checksum = "4121e36a8757dea6fb237435ee5095eba25bc13ecc2eaee57eb9bffd4b27784f" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1402,24 +1848,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b786958bcb79bdb5fbae095af58f0c2da7d7895c475c991f6a6bb5a9c7e6d9" +checksum = "5173265fc30b9e42205cf06dfca9a272ee949667ce4115d975ed2c08d466e2f8" [[package]] name = "cranelift-control" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2faf9a5009bce7f725ce2af7a08c4883ebac6af933e7e0aa7d84f976f4e6deb5" +checksum = "9b6ca8a393e66dc13f915c6f456bdcf496d78fac4995792f7022b7806352d7a4" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "017271194ba5e101d626560d0d6767efd341468d1ba0f4d015f19fe64020b65b" +checksum = "e609d9ba416bc26d774f343295a1d411515248a6a6d83d5f7492a0dd919569fa" dependencies = [ "cranelift-bitset", "serde", @@ -1428,9 +1874,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f80847f0929967f0cec82f9e0543b3901e0f0063690405891f22107b5a130fd8" +checksum = "90a3a277b1a0aff1123f6bae61c080a4bcb6df964829ed427f98e18dff14257f" dependencies = [ "cranelift-codegen", "log", @@ -1440,15 +1886,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75904abbc0e7b46d20f7a49c8042c8a4481c0db4253b99889c723c566295d506" +checksum = "be53dd9b3a4cbeb9ced45c4a543ea8417ccfb334c3ba1cbb2233f4b25fb2531f" [[package]] name = "cranelift-native" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0135923540574362e16f01bf40000664263840991039ff3041ba717de6cf3a" +checksum = "ca62ab1d9f48cad97da5843b913ccf96c3dfde935af5d750cb5a6d367ccfb262" dependencies = [ "cranelift-codegen", "libc", @@ -1457,9 +1903,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.123.12" +version = "0.123.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fb12f76c482e034f6ebefa843c914e74112f088215d8d36d33a649f9fab99b" +checksum = "a13b72860f54a2a19d3756bd47575fd8bddeafd6e5bbbf16372cdfebc482628a" [[package]] name = "crc" @@ -1710,6 +2156,398 @@ dependencies = [ "cmov", ] +[[package]] +name = "cubecl" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd203fef6e359472e4cb8f6c0ef3eca062815a1edd560eb6d6c1d5c1397838d9" +dependencies = [ + "cubecl-core", + "cubecl-cuda", + "cubecl-ir", + "cubecl-runtime", + "cubecl-std", + "cubecl-wgpu", + "half", +] + +[[package]] +name = "cubecl-common" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc956c2dcc993f16f748d03bfdbd900f75cab4d469f4e5d8924f8ee128e861ad" +dependencies = [ + "backtrace", + "bytemuck", + "bytes", + "cfg-if", + "cfg_aliases", + "ciborium", + "derive-new", + "derive_more 2.1.1", + "dirs 6.0.0", + "embassy-futures", + "embassy-time", + "float4", + "float8 0.7.0", + "futures-lite", + "half", + "hashbrown 0.16.1", + "log", + "num-traits", + "oneshot", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "rand 0.10.2", + "sanitize-filename", + "serde", + "serde_bytes", + "serde_json", + "spin 0.10.1", + "toml 1.1.2+spec-1.1.0", + "tynm", + "wasm-bindgen-futures", + "web-time", + "xxhash-rust", +] + +[[package]] +name = "cubecl-core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7522e7acf25d7848032c7b5270780f9f2abe84e13c7ed6345aa27ba70c312ca" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "cubecl-common", + "cubecl-ir", + "cubecl-macros", + "cubecl-runtime", + "cubecl-zspace", + "derive-new", + "derive_more 2.1.1", + "enumset", + "float-ord", + "half", + "hashbrown 0.16.1", + "log", + "num-traits", + "paste", + "serde", + "serde_json", + "variadics_please", +] + +[[package]] +name = "cubecl-cpp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411af04828cbf4583ecd388579fb30cd7a644eec19a334bb8f0d9d08522e1458" +dependencies = [ + "bytemuck", + "cubecl-common", + "cubecl-core", + "cubecl-opt", + "cubecl-runtime", + "derive-new", + "half", + "itertools 0.14.0", + "log", +] + +[[package]] +name = "cubecl-cuda" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b0a69ff45688d322ad8e92c8bf645167b9ca490fa8fa087fc6adac8c5e46be" +dependencies = [ + "bytemuck", + "cubecl-common", + "cubecl-core", + "cubecl-cpp", + "cubecl-runtime", + "cudarc 0.19.7", + "derive-new", + "half", + "log", + "serde", +] + +[[package]] +name = "cubecl-ir" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d28a897a40c8ee6c57a8b8d76d8823ba2e97f4c2b83db9338f17a0752132d486" +dependencies = [ + "cubecl-common", + "cubecl-macros-internal", + "derive-new", + "derive_more 2.1.1", + "enumset", + "float-ord", + "fnv", + "foldhash 0.2.0", + "half", + "hashbrown 0.16.1", + "num-traits", + "portable-atomic", + "serde", + "variadics_please", +] + +[[package]] +name = "cubecl-macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77aa6df563e8e6a0926d2e9eeacb968737940a0e1ae9be819f6a65a341006e12" +dependencies = [ + "cubecl-common", + "darling 0.23.0", + "derive-new", + "ident_case", + "inflections", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "cubecl-macros-internal" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a515651a5a91e25d87f71f311f80a088e6cf815798b9922560a97636da6b8740" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "cubecl-opt" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a599c6c3efefdcee8636994c90e58ef696c7efbed2d18b5b5ad8d5f29923abb" +dependencies = [ + "cubecl-common", + "cubecl-core", + "cubecl-ir", + "float-ord", + "log", + "num", + "petgraph 0.8.3", + "smallvec", + "stable-vec", + "type-map", +] + +[[package]] +name = "cubecl-runtime" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68491bf5b3e997ae36bdc4e63b4ccd6d2f0e86b3b596a5d7a48d2b9e92622a0" +dependencies = [ + "ahash", + "async-channel", + "bytemuck", + "cfg-if", + "cfg_aliases", + "cubecl-common", + "cubecl-ir", + "cubecl-zspace", + "derive-new", + "derive_more 2.1.1", + "dirs 6.0.0", + "enumset", + "hashbrown 0.16.1", + "log", + "md5", + "serde", + "serde_json", + "spin 0.10.1", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "cubecl-std" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b391b584a4897683dd9fc0767f96337ac712b733126ce0f68a9ee8a2df073d2d" +dependencies = [ + "cubecl-common", + "cubecl-core", + "cubecl-runtime", + "half", + "num-traits", + "paste", + "serde", + "spin 0.10.1", + "variadics_please", +] + +[[package]] +name = "cubecl-wgpu" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3663c6e1a86187172b2cee495b6b533e7d91a2cb37e26f421649e315fe4c3a" +dependencies = [ + "async-channel", + "bytemuck", + "cfg-if", + "cfg_aliases", + "cubecl-common", + "cubecl-core", + "cubecl-ir", + "cubecl-runtime", + "derive-new", + "derive_more 2.1.1", + "half", + "hashbrown 0.16.1", + "log", + "sanitize-filename", + "wasm-bindgen-futures", + "wgpu", +] + +[[package]] +name = "cubecl-zspace" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04054d95698afab785a4dda9f949e297831dd9e4cc6d036a93e6603f88e88b07" +dependencies = [ + "derive-new", + "serde", + "smallvec", +] + +[[package]] +name = "cubek" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62fb5043d5eb723017415eec57c541f41901beea984500e2e16b57d75fa717" +dependencies = [ + "cubecl", + "cubek-attention", + "cubek-convolution", + "cubek-fft", + "cubek-matmul", + "cubek-quant", + "cubek-random", + "cubek-reduce", + "cubek-std", +] + +[[package]] +name = "cubek-attention" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ca5c363f05a9297b9d1c0e90959cc5f9fd90bc596e4e537f26f20602ec63a5" +dependencies = [ + "bytemuck", + "cubecl", + "cubecl-common", + "cubek-matmul", + "cubek-std", + "half", + "serde", +] + +[[package]] +name = "cubek-convolution" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f2755a8eb9ea5509c17edac78e4a9dc4be89c433d8e2ee709703a93b96f749" +dependencies = [ + "bytemuck", + "cubecl", + "cubecl-common", + "cubek-matmul", + "cubek-std", + "derive-new", + "enumset", + "half", + "serde", +] + +[[package]] +name = "cubek-fft" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd7c2972363332dc4609067a2ccc21856308d98089e676d3f0c52747ae61a5b" +dependencies = [ + "cubecl", +] + +[[package]] +name = "cubek-matmul" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a4cea5f0f439907dc953c7638a6204b3f055f1bcbd10db91dfc5faa030ac1c" +dependencies = [ + "bytemuck", + "cubecl", + "cubecl-common", + "cubek-std", + "half", + "serde", +] + +[[package]] +name = "cubek-quant" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802ff1b5f19597a7b7cb308c7c69e9eef28b78d57dab7794c816bb8f7d3d1b85" +dependencies = [ + "cubecl", + "cubecl-common", + "half", + "serde", +] + +[[package]] +name = "cubek-random" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dfecce959bdbeb3b355120e586d1ea9e45a0fc7f2ca354f4260a571952551be" +dependencies = [ + "cubecl", + "cubecl-common", + "half", + "num-traits", + "rand 0.10.2", + "serde", +] + +[[package]] +name = "cubek-reduce" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86758b84452b06e9121f25200da13b4909ecd6235cefb5f33f459bea24dac41a" +dependencies = [ + "cubecl", + "cubek-std", + "half", + "num-traits", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "cubek-std" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a587d0a44b6f14f4c3711ac10717179b48adb59203b2b2bffe334876bf713187" +dependencies = [ + "cubecl", + "cubecl-common", + "half", +] + [[package]] name = "cudarc" version = "0.17.8" @@ -1758,14 +2596,48 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + [[package]] name = "darling" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", ] [[package]] @@ -1782,13 +2654,48 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -1809,9 +2716,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "debugid" @@ -1843,6 +2750,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive-new" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -1860,13 +2778,36 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", "syn 2.0.117", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + [[package]] name = "difflib" version = "0.4.0" @@ -1982,6 +2923,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading 0.8.9", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -2005,12 +2955,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "dpi" version = "0.1.2" @@ -2150,6 +3115,37 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" + +[[package]] +name = "embassy-time" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b0c143ec626e821d4d90da51a2bd91d559d6c442b7c74a47d368c9e23d97a" +dependencies = [ + "cfg-if", + "critical-section", + "document-features", + "embassy-time-driver", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "futures-core", +] + +[[package]] +name = "embassy-time-driver" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee71af1b3a0deaa53eaf2d39252f83504c853646e472400b763060389b9fcc9" +dependencies = [ + "document-features", +] + [[package]] name = "embed-resource" version = "3.0.6" @@ -2170,6 +3166,31 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -2182,6 +3203,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -2209,6 +3239,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "enumset" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc5801fd11762e24d1e420d01d2ac518f2a2ca4329d4fbb6639f2412b6204e0" +dependencies = [ + "enumset_derive", + "serde", +] + +[[package]] +name = "enumset_derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "env_filter" version = "1.0.1" @@ -2232,6 +3284,26 @@ dependencies = [ "log", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2256,7 +3328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2272,15 +3344,24 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -2363,6 +3444,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -2383,6 +3470,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float4" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a5404bf31d22893d61cf24d4dda149d8e6b2ff07601c3cb3be651031f61a4ed" + [[package]] name = "float8" version = "0.6.1" @@ -2420,7 +3519,7 @@ dependencies = [ "futures-core", "futures-sink", "nanorand", - "spin", + "spin 0.9.8", ] [[package]] @@ -2561,6 +3660,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -2584,6 +3696,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.32" @@ -3245,6 +4363,148 @@ dependencies = [ "winapi", ] +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[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 = "glam" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360bd2cd76e0cd9032d42cf2922155cecea2685b0cfa4630c3246df030bcfd6" +dependencies = [ + "approx", + "libm", +] + +[[package]] +name = "glamx" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9847f980afb0628849bcd6a406cdffd29eb9d53fca83e1646005901a9acd4c5c" +dependencies = [ + "approx", + "glam 0.33.3", + "nalgebra 0.35.0", + "num-traits", + "simba 0.10.2", +] + [[package]] name = "glib" version = "0.18.5" @@ -3298,6 +4558,27 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -3309,6 +4590,40 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.18", + "windows 0.62.2", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.11.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "group" version = "0.13.0" @@ -3422,6 +4737,7 @@ dependencies = [ "num-traits", "rand 0.9.2", "rand_distr 0.5.1", + "serde", "zerocopy", ] @@ -3458,6 +4774,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -3489,6 +4814,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + [[package]] name = "hashlink" version = "0.10.0" @@ -3529,7 +4863,7 @@ dependencies = [ "atomic-polyfill", "hash32 0.2.1", "rustc_version", - "spin", + "spin 0.9.8", "stable_deref_trait", ] @@ -3567,6 +4901,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hkdf" version = "0.12.4" @@ -4233,6 +5573,12 @@ dependencies = [ "cfb", ] +[[package]] +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + [[package]] name = "inout" version = "0.1.4" @@ -4294,7 +5640,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4514,6 +5860,23 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading 0.8.9", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -4555,7 +5918,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -4685,6 +6048,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -4727,6 +6096,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "macerator" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ddeaadb76e307b1d2b4836adee77910d405b9f6c1ca031bc81a7a398e2b22f2" +dependencies = [ + "bytemuck", + "cfg_aliases", + "half", + "macerator-macros", + "moddef", + "num-traits", + "paste", + "rustc_version", +] + +[[package]] +name = "macerator-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a397d020af346358830d6f28579f26283003ce87af3dd79173ac9ee2d3ac28" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "mach2" version = "0.4.3" @@ -4801,7 +6198,7 @@ dependencies = [ "bitflags 1.3.2", "mavlink-bindgen", "mavlink-core", - "num-derive", + "num-derive 0.3.3", "num-traits", "serde", "serde_arrays", @@ -4844,6 +6241,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "md5" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" + [[package]] name = "mdns-sd" version = "0.11.5" @@ -4904,7 +6307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bebe548a4e74b80ecb8dd058e352a91fed9e5685c49c5d3fa5062520c660c6c9" dependencies = [ "midstreamer-temporal-compare", - "nalgebra", + "nalgebra 0.33.2", "ndarray 0.16.1", "serde", "thiserror 2.0.18", @@ -5051,6 +6454,12 @@ dependencies = [ "windows 0.48.0", ] +[[package]] +name = "moddef" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0b3262dc837d2513fe2ef31ff8461352ef932dcca31ba0c0abe33547cf6b9b" + [[package]] name = "muda" version = "0.17.1" @@ -5092,6 +6501,32 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "naga" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap 2.13.0", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.18", + "unicode-ident", +] + [[package]] name = "nalgebra" version = "0.33.2" @@ -5100,11 +6535,65 @@ checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" dependencies = [ "approx", "matrixmultiply", - "nalgebra-macros", + "nalgebra-macros 0.2.2", "num-complex", "num-rational", "num-traits", - "simba", + "simba 0.9.1", + "typenum", +] + +[[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 0.3.0", + "num-complex", + "num-rational", + "num-traits", + "simba 0.9.1", + "typenum", +] + +[[package]] +name = "nalgebra" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc43a60c217b0c6ff46e47f26911015ad8d2e5a8be1af668c67e370d99a4346" +dependencies = [ + "approx", + "glam 0.30.10", + "glam 0.31.1", + "glam 0.32.1", + "glam 0.33.3", + "matrixmultiply", + "nalgebra-macros 0.3.0", + "num-complex", + "num-rational", + "num-traits", + "simba 0.10.2", "typenum", ] @@ -5119,6 +6608,17 @@ dependencies = [ "syn 2.0.117", ] +[[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.117", +] + [[package]] name = "nanorand" version = "0.7.0" @@ -5145,6 +6645,21 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + [[package]] name = "ndarray" version = "0.15.6" @@ -5310,6 +6825,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -5403,6 +6927,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -5604,6 +7139,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -5614,6 +7161,7 @@ dependencies = [ "objc2", "objc2-core-foundation", "objc2-foundation", + "objc2-metal", ] [[package]] @@ -5666,6 +7214,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oneshot" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c" + [[package]] name = "oorandom" version = "11.1.5" @@ -5847,6 +7401,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + [[package]] name = "ort" version = "2.0.0-rc.11" @@ -5878,7 +7441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.45.0", ] [[package]] @@ -5960,6 +7523,35 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "parry3d" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15fa77de549af010eddaf4bcb365b39aee96f93f65489b9a49b2de616fc664e8" +dependencies = [ + "approx", + "arrayvec", + "bitflags 2.11.0", + "downcast-rs", + "either", + "ena", + "foldhash 0.2.0", + "glamx", + "hashbrown 0.17.1", + "indexmap 2.13.0", + "log", + "num-derive 0.4.2", + "num-traits", + "ordered-float 5.3.0", + "rstar 0.13.0", + "simba 0.10.2", + "slab", + "smallvec", + "spade", + "static_assertions", + "thiserror 2.0.18", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -6075,10 +7667,22 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", + "fixedbitset 0.4.2", "indexmap 2.13.0", ] +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset 0.5.7", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "serde", +] + [[package]] name = "phf" version = "0.8.0" @@ -6364,12 +7968,15 @@ name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "serde", +] [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -6446,6 +8053,12 @@ dependencies = [ "termtree", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "prettyplease" version = "0.2.37" @@ -6563,14 +8176,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "proptest" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ - "bit-set", - "bit-vec", + "bit-set 0.8.0", + "bit-vec 0.8.0", "bitflags 2.11.0", "num-traits", "rand 0.9.2", @@ -6599,7 +8231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.117", @@ -6636,9 +8268,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558181096e0df4984f45cfc3a7087052df4a61c36089b135a08ceca9cbd352fb" +checksum = "2662666315cb90dfb4d99a652ee053d4d8598f71c474209e84da031ca56ae5a4" dependencies = [ "cranelift-bitset", "log", @@ -6648,9 +8280,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5d52e2f14e168d75cdabe9bd5fb1ff18a1b119dc6699684aee895dbc3524da9" +checksum = "bb9a7d9ed2618f94b6d054aba3eb2768c9b489fe16b4ef8847fbc6ed41b707bb" dependencies = [ "proc-macro2", "quote", @@ -6754,7 +8386,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.37", "socket2 0.6.2", "thiserror 2.0.18", @@ -6775,7 +8407,7 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.37", "rustls-pki-types", "rustls-platform-verifier", @@ -6797,14 +8429,14 @@ dependencies = [ "once_cell", "socket2 0.6.2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -6859,6 +8491,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20 0.10.1", + "getrandom 0.4.1", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -6942,6 +8585,16 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "rand_distr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" +dependencies = [ + "num-traits", + "rand 0.10.2", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -6969,6 +8622,33 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rapier3d" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9038df6d760a6448b39adf6286744acdb54e2a0edf04b8ab6a9f5e9998968317" +dependencies = [ + "approx", + "bitflags 2.11.0", + "glamx", + "log", + "nalgebra 0.35.0", + "num-traits", + "parry3d", + "profiling", + "serde", + "simba 0.10.2", + "static_assertions", + "thiserror 2.0.18", + "wide 1.6.1", +] + [[package]] name = "raw-cpuid" version = "10.7.0" @@ -6993,6 +8673,18 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -7116,7 +8808,7 @@ dependencies = [ "bumpalo", "hashbrown 0.15.5", "log", - "rustc-hash", + "rustc-hash 2.1.1", "smallvec", ] @@ -7149,6 +8841,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "rend" version = "0.5.3" @@ -7158,6 +8856,12 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "reqwest" version = "0.11.27" @@ -7329,13 +9033,13 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.8.15" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" +checksum = "815cc8a37159a463064825246cadb07961e25cd9885908606f6d08a98d8f8874" dependencies = [ "bytecheck", "bytes", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap 2.13.0", "munge", "ptr_meta", @@ -7348,15 +9052,34 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.8.15" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" +checksum = "c0ed1a78a1b19d184b0daa629dd9a024573173ec7d485b287cb369fb3607cc1c" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + [[package]] name = "roaring" version = "0.10.12" @@ -7454,6 +9177,46 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rstar" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5912b862fa5ffb462607bfd1e35036c458c537921f508c8235a83d5f3987edfe" +dependencies = [ + "heapless 0.8.0", + "num-traits", + "smallvec", +] + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.117", + "unicode-ident", +] + [[package]] name = "rufield-core" version = "0.1.0" @@ -7514,6 +9277,12 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -7553,7 +9322,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7668,7 +9437,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7835,9 +9604,9 @@ dependencies = [ "anyhow", "crossbeam", "dashmap", - "ordered-float", + "ordered-float 4.6.0", "parking_lot", - "petgraph", + "petgraph 0.6.5", "rand 0.8.5", "rayon", "roaring", @@ -8065,7 +9834,7 @@ dependencies = [ "criterion", "hmac", "mavlink", - "nalgebra", + "nalgebra 0.33.2", "ort", "rand 0.8.5", "reqwest 0.12.28", @@ -8140,6 +9909,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "safe_arch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c6efa15875e6ecb39ca61fb0b0c1a40b84fac5a5ffe71eef7d1000c8eb3f5f" +dependencies = [ + "bytemuck", +] + [[package]] name = "safetensors" version = "0.3.3" @@ -8180,6 +9958,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sanitize-filename" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" +dependencies = [ + "regex", +] + [[package]] name = "schannel" version = "0.1.28" @@ -8314,7 +10101,7 @@ checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", "cssparser", - "derive_more", + "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", @@ -8382,6 +10169,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -8503,7 +10300,7 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.117", @@ -8735,7 +10532,20 @@ dependencies = [ "num-complex", "num-traits", "paste", - "wide", + "wide 0.7.33", +] + +[[package]] +name = "simba" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a7200d82ff1c7b4235efd12f11e2537a402c91cf83a5cb97ce80eef787fde7" +dependencies = [ + "approx", + "libm", + "num-complex", + "num-traits", + "wide 1.6.1", ] [[package]] @@ -8789,6 +10599,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -8898,6 +10717,25 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +dependencies = [ + "lock_api", + "portable-atomic", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "spki" version = "0.7.3" @@ -9117,6 +10955,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "stable-vec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dac7bc0f7d0d44329b200020effbc25a534d89fa142af95e3ddf76113412a5e" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -9177,6 +11021,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "subtle" version = "2.6.1" @@ -9194,6 +11059,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -9758,7 +11629,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -9806,6 +11677,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "text_placeholder" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5008f74a09742486ef0047596cf35df2b914e2a8dca5727fcb6ba6842a766b" +dependencies = [ + "hashbrown 0.13.2", + "serde", + "serde_json", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -9886,6 +11768,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -10344,6 +12235,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -10388,9 +12292,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -10463,6 +12367,24 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tynm" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21cdb0fc8f85c98b1ec812bc4cd69faf6c0fa2fc17d44ea3c2cdd38dc08e999" +dependencies = [ + "nom 8.0.0", +] + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + [[package]] name = "typed-path" version = "0.12.3" @@ -10748,9 +12670,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" dependencies = [ "getrandom 0.4.1", "js-sys", @@ -10764,6 +12686,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "variadics_please" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -10788,6 +12721,12 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "vswhom" version = "0.1.0" @@ -11095,9 +13034,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4442dc12aa2473def8334f0e0f2b489be52c52507c938bbdc8be69ded4ded6" +checksum = "93d881c5dcff5f230368d84fcf110ca25fe47badc694e7d559c3891492159916" dependencies = [ "addr2line", "anyhow", @@ -11150,9 +13089,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d881c3d6205898a226cc487b117f23b9ed1c7da39952d65bd5eeb6745b3789c" +checksum = "a4b25534a3ff9dd844701c2bc997f76cb1f97bf2af0546a7066ae96f49c2e068" dependencies = [ "anyhow", "cpp_demangle", @@ -11177,18 +13116,18 @@ dependencies = [ [[package]] name = "wasmtime-internal-asm-macros" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab1876bcfa51d6a05dea1c13933f53cbc1e316c783fddebc859f56a736eae07" +checksum = "1d02832d760351fb3a1aa99eb8f7b95596c0f4e4e52df1547fcdb295ea5c780c" dependencies = [ "cfg-if", ] [[package]] name = "wasmtime-internal-cache" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2195a1521a5214a71b67ca2ba2d9317ae15917288ea9d8e5836142b921c9d155" +checksum = "651f8a16b51cde3ee8bd5b775bcedc24b66040d7dca1f13ad855b10c660e1d01" dependencies = [ "anyhow", "base64 0.22.1", @@ -11206,9 +13145,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae1407944a0b13a8a77930b5b951aa7134beccecad7efac1ef9f03adb7d1a0f" +checksum = "ba2ed6dbe24607573f7bcc59222f3bc2e7f7d31609466055c67b9484045a374a" dependencies = [ "anyhow", "proc-macro2", @@ -11221,15 +13160,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "646a53678ce6aaf6f097e18ca51f650f2841aea6d2bcd7b61931397b8b8f30db" +checksum = "e21aadbf677ee3503ef2580ce3c6955508e90a2810e961739b30f79585af254c" [[package]] name = "wasmtime-internal-cranelift" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3495aa8300e4ca6b53f81a53ce5eff6621fd5ff8378ef9ae552d1479d57371" +checksum = "65821bab751956cddfc6ee957beb13a0e2a6cf682050d75dfb7a0228db2ed499" dependencies = [ "anyhow", "cfg-if", @@ -11254,9 +13193,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29b5e4023a6b167da157338f5f0f505945eb45e78f1cac2d4dcce0922457d7d4" +checksum = "18cf73e5e8d28a2b30d86454430c7dea3b593598dcbbc0ebb927ca2716222d41" dependencies = [ "anyhow", "cc", @@ -11270,9 +13209,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da71e2d573e3cc6f753a3b7bff98f425ca060c0e8071cc55c3d867a9edf3ecc" +checksum = "4279dc3147ddaa21e5b3d2c0eaff117881c4f937747bdd2379eb361665843bd3" dependencies = [ "cc", "object", @@ -11282,9 +13221,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "627d8f57909a4f9bb1dbe57a96229a54b89d5995353d0b321f3cb9a1a118977a" +checksum = "a8eb677944201839c0be19b39b0f19dcd663efbcbc05a09c73f26fec7bdf0331" dependencies = [ "anyhow", "cfg-if", @@ -11294,24 +13233,24 @@ dependencies = [ [[package]] name = "wasmtime-internal-math" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45b99315585a8a27125dd9b0150edb115d6f6ff0baae453c21d30822aab77f00" +checksum = "db9a153e184878df396a11f8912444531c2e4d0c7a1d9e9a52b30d9853d89cb9" dependencies = [ "libm", ] [[package]] name = "wasmtime-internal-slab" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaee97281dd3fe47ec3d46c16fb9fe2dd32f37d0523c2d5c484f11b348734e4" +checksum = "b771494bead25e1f0c4c89a9476dd0b65eacca314ed82125f1e197fbc3f0396b" [[package]] name = "wasmtime-internal-unwinder" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c005f82c48492b6b44fa19ee5205bd933c4f8baca41e314eca8331dd3c4fd9" +checksum = "90199caed6925420434a923861d8ad813689ca8533d2c679f805d12e35b40a4b" dependencies = [ "anyhow", "cfg-if", @@ -11322,9 +13261,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b73639a9c0c0e33a2ef942ca99b6772b48393be92bebbd0767c607e5b0a68e0" +checksum = "ae71687aa834f9cc9eb5b0f97c85184d7dc70b84d32fd8927af0887998a1ba35" dependencies = [ "proc-macro2", "quote", @@ -11333,9 +13272,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "392ca021d084c7426616ef77e1284315555f11bcbb34f416d74b0732db622811" +checksum = "5d0ba45c0d766dd56257d97702d3284b6f69efdd4c53ca981a0754cc0a139df0" dependencies = [ "anyhow", "cranelift-codegen", @@ -11350,9 +13289,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd4703351476262d715b72431e80d10289908e3494050071d6521267f522d97" +checksum = "41a64d5112c06f9d54b41e61f0b757d3a5a52361bf359f01131cc7cb8551ac73" dependencies = [ "anyhow", "bitflags 2.11.0", @@ -11383,6 +13322,18 @@ dependencies = [ "wast", ] +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.91" @@ -11516,6 +13467,174 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "wgpu" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +dependencies = [ + "arrayvec", + "bitflags 2.11.0", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bit-vec 0.9.1", + "bitflags 2.11.0", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap 2.13.0", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.9.1", + "bitflags 2.11.0", + "block2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.9", + "log", + "naga", + "ndk-sys", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", + "objc2-quartz-core", + "once_cell", + "ordered-float 5.3.0", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.18", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-result 0.4.1", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + [[package]] name = "whoami" version = "1.6.1" @@ -11533,7 +13652,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" dependencies = [ "bytemuck", - "safe_arch", + "safe_arch 0.7.4", +] + +[[package]] +name = "wide" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2aaf408e58689c2096682331b1f42bb2d9f2ed6b11560407d023cd0a6c634e" +dependencies = [ + "bytemuck", + "safe_arch 1.2.0", ] [[package]] @@ -11588,7 +13717,7 @@ dependencies = [ "axum", "chrono", "clap", - "colored", + "colored 2.2.0", "console 0.16.3", "csv", "indicatif", @@ -11776,6 +13905,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "wifi-densepose-physics" +version = "0.1.0" +dependencies = [ + "bitflags 2.11.0", + "blake3", + "burn-core", + "burn-cuda", + "burn-ndarray", + "burn-nn", + "burn-train", + "burn-wgpu", + "criterion", + "nalgebra 0.34.2", + "proptest", + "rapier3d", + "serde", + "serde_json", + "smallvec", + "thiserror 2.0.18", + "tracing", + "wifi-densepose-core", +] + [[package]] name = "wifi-densepose-pointcloud" version = "0.1.0" @@ -11880,9 +14033,11 @@ dependencies = [ "ureq 2.12.1", "wifi-densepose-aether", "wifi-densepose-bfld", + "wifi-densepose-core", "wifi-densepose-engine", "wifi-densepose-geo", "wifi-densepose-hardware", + "wifi-densepose-physics", "wifi-densepose-rufield", "wifi-densepose-signal", "wifi-densepose-wifiscan", @@ -11932,7 +14087,7 @@ dependencies = [ "ndarray-npy", "num-complex", "num-traits", - "petgraph", + "petgraph 0.6.5", "proptest", "ruvector-attention 2.1.0", "ruvector-attn-mincut", @@ -11999,7 +14154,7 @@ dependencies = [ name = "wifi-densepose-worldgraph" version = "0.3.2" dependencies = [ - "petgraph", + "petgraph 0.6.5", "serde", "serde_json", "thiserror 2.0.18", @@ -12044,9 +14199,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "36.0.12" +version = "36.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ec880b20caaa72245944b54cfb22aca111f8c805e12a7542b40d66921e5323" +checksum = "82ea9625459ce35d6a4188cf0f07c9095cbb0e5afd86f711d63955bfc4216e64" dependencies = [ "anyhow", "cranelift-assembler-x64", @@ -12102,11 +14257,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -12118,6 +14285,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -12164,7 +14340,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -12233,6 +14420,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -12415,6 +14612,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -12848,6 +15054,18 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" + [[package]] name = "yasna" version = "0.5.2" diff --git a/v2/Cargo.toml b/v2/Cargo.toml index 9f553f32..58932c20 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -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" } diff --git a/v2/crates/cog-pose-estimation/Cargo.toml b/v2/crates/cog-pose-estimation/Cargo.toml index f01b8626..14cdf082 100644 --- a/v2/crates/cog-pose-estimation/Cargo.toml +++ b/v2/crates/cog-pose-estimation/Cargo.toml @@ -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" diff --git a/v2/crates/cog-pose-estimation/src/inference.rs b/v2/crates/cog-pose-estimation/src/inference.rs index 314d75e5..6d934084 100644 --- a/v2/crates/cog-pose-estimation/src/inference.rs +++ b/v2/crates/cog-pose-estimation/src/inference.rs @@ -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 { + 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.5–3.6). Low-rank deltas on the pose @@ -185,6 +267,7 @@ impl PoseNet { pub struct InferenceEngine { inner: Option>, device: Device, + artifact_hash: [u8; 32], } struct LoadedModel { @@ -227,6 +310,7 @@ impl InferenceEngine { adapter_path: Option<&Path>, ) -> Result> { 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> { 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> { + 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 { // Search in the order an installed Cog would see it. let candidates = [ diff --git a/v2/crates/cog-pose-estimation/src/main.rs b/v2/crates/cog-pose-estimation/src/main.rs index 669eedfd..31be49e2 100644 --- a/v2/crates/cog-pose-estimation/src/main.rs +++ b/v2/crates/cog-pose-estimation/src/main.rs @@ -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"); } diff --git a/v2/crates/cog-pose-estimation/src/publisher.rs b/v2/crates/cog-pose-estimation/src/publisher.rs index bc4951c8..232cec16 100644 --- a/v2/crates/cog-pose-estimation/src/publisher.rs +++ b/v2/crates/cog-pose-estimation/src/publisher.rs @@ -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, }), } } diff --git a/v2/crates/cog-pose-estimation/src/runtime.rs b/v2/crates/cog-pose-estimation/src/runtime.rs index fc71d872..3139bf1d 100644 --- a/v2/crates/cog-pose-estimation/src/runtime.rs +++ b/v2/crates/cog-pose-estimation/src/runtime.rs @@ -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> { let mut buffer: Vec = 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)); + } } } } diff --git a/v2/crates/cog-pose-estimation/tests/smoke.rs b/v2/crates/cog-pose-estimation/tests/smoke.rs index c79a559c..254d77b2 100644 --- a/v2/crates/cog-pose-estimation/tests/smoke.rs +++ b/v2/crates/cog-pose-estimation/tests/smoke.rs @@ -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 diff --git a/v2/crates/wifi-densepose-core/src/lib.rs b/v2/crates/wifi-densepose-core/src/lib.rs index 514d2ce6..7cbcd754 100644 --- a/v2/crates/wifi-densepose-core/src/lib.rs +++ b/v2/crates/wifi-densepose-core/src/lib.rs @@ -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, diff --git a/v2/crates/wifi-densepose-core/src/pose_observation_v2.rs b/v2/crates/wifi-densepose-core/src/pose_observation_v2.rs new file mode 100644 index 00000000..8d3b1cdb --- /dev/null +++ b/v2/crates/wifi-densepose-core/src/pose_observation_v2.rs @@ -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 { + 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 for Probability { + type Error = &'static str; + fn try_from(value: f32) -> Result { + Self::new(value) + } +} + +impl From 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::(); + (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, + 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()); + } +} diff --git a/v2/crates/wifi-densepose-core/src/pose_refinement_v1.rs b/v2/crates/wifi-densepose-core/src/pose_refinement_v1.rs new file mode 100644 index 00000000..85732611 --- /dev/null +++ b/v2/crates/wifi-densepose-core/src/pose_refinement_v1.rs @@ -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, + /// Present only when the separately gated Rapier auditor ran. + pub dynamics: Option, + pub contact_hypotheses: [ContactHypothesis; 2], + pub provenance: PhysicsProvenance, + pub reason: Option, + /// 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()); +} diff --git a/v2/crates/wifi-densepose-physics/Cargo.toml b/v2/crates/wifi-densepose-physics/Cargo.toml new file mode 100644 index 00000000..f097df1f --- /dev/null +++ b/v2/crates/wifi-densepose-physics/Cargo.toml @@ -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" diff --git a/v2/crates/wifi-densepose-physics/benches/frame_latency.rs b/v2/crates/wifi-densepose-physics/benches/frame_latency.rs new file mode 100644 index 00000000..8867c196 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/benches/frame_latency.rs @@ -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); diff --git a/v2/crates/wifi-densepose-physics/examples/latency_probe.rs b/v2/crates/wifi-densepose-physics/examples/latency_probe.rs new file mode 100644 index 00000000..14ee5d8a --- /dev/null +++ b/v2/crates/wifi-densepose-physics/examples/latency_probe.rs @@ -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> { + 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 +} diff --git a/v2/crates/wifi-densepose-physics/examples/verify_golden.rs b/v2/crates/wifi-densepose-physics/examples/verify_golden.rs new file mode 100644 index 00000000..57ff3764 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/examples/verify_golden.rs @@ -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> { + let mut arguments = std::env::args().skip(1); + let path = arguments + .next() + .ok_or("usage: verify_golden [--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 +} diff --git a/v2/crates/wifi-densepose-physics/fuzz/.gitignore b/v2/crates/wifi-densepose-physics/fuzz/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/fuzz/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/v2/crates/wifi-densepose-physics/fuzz/Cargo.lock b/v2/crates/wifi-densepose-physics/fuzz/Cargo.lock new file mode 100644 index 00000000..db31cf42 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/fuzz/Cargo.lock @@ -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" diff --git a/v2/crates/wifi-densepose-physics/fuzz/Cargo.toml b/v2/crates/wifi-densepose-physics/fuzz/Cargo.toml new file mode 100644 index 00000000..32cdb442 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/fuzz/Cargo.toml @@ -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 diff --git a/v2/crates/wifi-densepose-physics/fuzz/fuzz_targets/pose_frame.rs b/v2/crates/wifi-densepose-physics/fuzz/fuzz_targets/pose_frame.rs new file mode 100644 index 00000000..6d0141de --- /dev/null +++ b/v2/crates/wifi-densepose-physics/fuzz/fuzz_targets/pose_frame.rs @@ -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::(bytes) { + if let Ok(mut engine) = PhysicsEngine::new(PhysicsConfig::default()) { + let _ = engine.process(&raw, raw.timestamp_ns); + } + } +}); diff --git a/v2/crates/wifi-densepose-physics/src/config.rs b/v2/crates/wifi-densepose-physics/src/config.rs new file mode 100644 index 00000000..0021b194 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/config.rs @@ -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() + } +} diff --git a/v2/crates/wifi-densepose-physics/src/constraints/bone.rs b/v2/crates/wifi-densepose-physics/src/constraints/bone.rs new file mode 100644 index 00000000..c2cba810 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/constraints/bone.rs @@ -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::(); + 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 +} diff --git a/v2/crates/wifi-densepose-physics/src/constraints/contact.rs b/v2/crates/wifi-densepose-physics/src/constraints/contact.rs new file mode 100644 index 00000000..a1b65bb5 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/constraints/contact.rs @@ -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) +} diff --git a/v2/crates/wifi-densepose-physics/src/constraints/floor.rs b/v2/crates/wifi-densepose-physics/src/constraints/floor.rs new file mode 100644 index 00000000..bffc6c2e --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/constraints/floor.rs @@ -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; + } + } +} diff --git a/v2/crates/wifi-densepose-physics/src/constraints/joint_limit.rs b/v2/crates/wifi-densepose-physics/src/constraints/joint_limit.rs new file mode 100644 index 00000000..13a28b6d --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/constraints/joint_limit.rs @@ -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() +} diff --git a/v2/crates/wifi-densepose-physics/src/constraints/mod.rs b/v2/crates/wifi-densepose-physics/src/constraints/mod.rs new file mode 100644 index 00000000..48c901fb --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/constraints/mod.rs @@ -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, + } +} diff --git a/v2/crates/wifi-densepose-physics/src/constraints/temporal.rs b/v2/crates/wifi-densepose-physics/src/constraints/temporal.rs new file mode 100644 index 00000000..5c7e6bce --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/constraints/temporal.rs @@ -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 +} diff --git a/v2/crates/wifi-densepose-physics/src/dynamics/mod.rs b/v2/crates/wifi-densepose-physics/src/dynamics/mod.rs new file mode 100644 index 00000000..cf14ad4f --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/dynamics/mod.rs @@ -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, + anchors: Vec, + 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 { + [first.0, first.1] + .into_iter() + .find(|endpoint| *endpoint == second.0 || *endpoint == second.1) +} diff --git a/v2/crates/wifi-densepose-physics/src/engine.rs b/v2/crates/wifi-densepose-physics/src/engine.rs new file mode 100644 index 00000000..272aad67 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/engine.rs @@ -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, + last_timestamp_ns: Option, + last_hash: Option<[u8; 32]>, + last_config_hash: Option<[u8; 32]>, + cached: Option, + skeleton: SkeletonPosterior, + temporal: TemporalHistory, + last_calibration_id: Option, + #[cfg(feature = "dynamics")] + dynamics: Option, +} + +/// 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 { + 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 { + 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, +} + +impl PhysicsEngine { + /// Activate a validated trusted configuration. + pub fn new(config: PhysicsConfig) -> Result { + 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 = 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 { + 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, + ) -> 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, + reason: Option, + ) -> 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::() / lengths.len() as f32; + let ratio_variance = if mean > 1.0e-6 { + lengths + .iter() + .map(|length| ((length - mean) / mean).powi(2)) + .sum::() + / 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) + } +} diff --git a/v2/crates/wifi-densepose-physics/src/error.rs b/v2/crates/wifi-densepose-physics/src/error.rs new file mode 100644 index 00000000..0a70323b --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/error.rs @@ -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), +} diff --git a/v2/crates/wifi-densepose-physics/src/learned/artifact.rs b/v2/crates/wifi-densepose-physics/src/learned/artifact.rs new file mode 100644 index 00000000..db68b1c3 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/learned/artifact.rs @@ -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 { + (!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, +} + +impl LearnedArtifact { + pub fn verified( + bytes: Vec, + expected: [u8; 32], + manifest: LearnedArtifactManifest, + signature: SignatureVerification, + maximum_bytes: usize, + ) -> Result { + 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 { + 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) + } +} diff --git a/v2/crates/wifi-densepose-physics/src/learned/features.rs b/v2/crates/wifi-densepose-physics/src/learned/features.rs new file mode 100644 index 00000000..771d0f35 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/learned/features.rs @@ -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, +} + +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(¤t, 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, +} + +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> { + 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) + }) +} diff --git a/v2/crates/wifi-densepose-physics/src/learned/mod.rs b/v2/crates/wifi-densepose-physics/src/learned/mod.rs new file mode 100644 index 00000000..3a699937 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/learned/mod.rs @@ -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; diff --git a/v2/crates/wifi-densepose-physics/src/learned/model.rs b/v2/crates/wifi-densepose-physics/src/learned/model.rs new file mode 100644 index 00000000..1b9b9944 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/learned/model.rs @@ -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 { + pub joint_residual: Tensor, + pub residual_log_variance: Tensor, + pub contact_probability: Tensor, + pub abstention_probability: Tensor, +} + +/// Two-layer unidirectional GRU followed by independent auditable heads. +#[derive(Module, Debug)] +pub struct ResidualGru { + gru1: Gru, + gru2: Gru, + residual_head: Linear, + variance_head: Linear, + contact_head: Linear, + abstention_head: Linear, + tanh: Tanh, + sigmoid: Sigmoid, +} + +impl ResidualGru { + /// 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 { + let recorder = NamedMpkBytesRecorder::::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, RecorderError> { + NamedMpkBytesRecorder::::default().record(self.into_record(), ()) + } + + /// Forward `[batch, 20, FEATURE_WIDTH]` to four bounded output heads. + #[must_use] + pub fn forward(&self, input: Tensor) -> ResidualModelOutput { + 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)), + } + } +} diff --git a/v2/crates/wifi-densepose-physics/src/learned/runtime.rs b/v2/crates/wifi-densepose-physics/src/learned/runtime.rs new file mode 100644 index 00000000..4ed6cf5b --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/learned/runtime.rs @@ -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; + +/// Native CPU residual inference. Construction requires a verified artifact. +pub struct CpuResidualRuntime { + model: ResidualGru, + device: NdArrayDevice, + artifact_hash: [u8; 32], +} + +impl CpuResidualRuntime { + /// Atomically deserialize a verified Burn record before activation. + pub fn activate(artifact: &LearnedArtifact) -> Result { + 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 { + if flattened_history.len() != HISTORY_FRAMES * FEATURE_WIDTH + || !flattened_history.iter().all(|value| value.is_finite()) + { + return None; + } + let input = Tensor::::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::().ok()?; + let log_variance = output + .residual_log_variance + .into_data() + .to_vec::() + .ok()?; + let contact = output + .contact_probability + .into_data() + .to_vec::() + .ok()?; + let abstention = output + .abstention_probability + .into_data() + .to_vec::() + .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) + } +} diff --git a/v2/crates/wifi-densepose-physics/src/learned/training.rs b/v2/crates/wifi-densepose-physics/src/learned/training.rs new file mode 100644 index 00000000..5f32e82d --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/learned/training.rs @@ -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 { + 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 + }) + } +} diff --git a/v2/crates/wifi-densepose-physics/src/lib.rs b/v2/crates/wifi-densepose-physics/src/lib.rs new file mode 100644 index 00000000..ae36375a --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/lib.rs @@ -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; diff --git a/v2/crates/wifi-densepose-physics/src/metrics.rs b/v2/crates/wifi-densepose-physics/src/metrics.rs new file mode 100644 index 00000000..832c7a1f --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/metrics.rs @@ -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, + dropped: u64, + latest: Option, +} + +/// Thread-safe, bounded metrics registry with no person, joint, or room labels. +#[derive(Default)] +pub struct PhysicsMetrics { + aggregate: Mutex, +} + +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, + } + } +} diff --git a/v2/crates/wifi-densepose-physics/src/projector/mod.rs b/v2/crates/wifi-densepose-physics/src/projector/mod.rs new file mode 100644 index 00000000..c42ace6c --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/projector/mod.rs @@ -0,0 +1,5 @@ +//! Bounded position-based projection. + +mod pbd; + +pub use pbd::{project, Projection}; diff --git a/v2/crates/wifi-densepose-physics/src/projector/pbd.rs b/v2/crates/wifi-densepose-physics/src/projector/pbd.rs new file mode 100644 index 00000000..cac6f036 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/projector/pbd.rs @@ -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 { + 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::() / parent_indices.len() as f32; + let child_weight = + child_indices.iter().map(|&i| weights[i]).sum::() / 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) + } +} diff --git a/v2/crates/wifi-densepose-physics/src/skeleton.rs b/v2/crates/wifi-densepose-physics/src/skeleton.rs new file mode 100644 index 00000000..93d2e6ca --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/skeleton.rs @@ -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, + ] +} diff --git a/v2/crates/wifi-densepose-physics/src/uncertainty.rs b/v2/crates/wifi-densepose-physics/src/uncertainty.rs new file mode 100644 index 00000000..21625b36 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/src/uncertainty.rs @@ -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) +} diff --git a/v2/crates/wifi-densepose-physics/tests/common/mod.rs b/v2/crates/wifi-densepose-physics/tests/common/mod.rs new file mode 100644 index 00000000..e032003b --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/common/mod.rs @@ -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 +} diff --git a/v2/crates/wifi-densepose-physics/tests/contracts.rs b/v2/crates/wifi-densepose-physics/tests/contracts.rs new file mode 100644 index 00000000..1d245cc7 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/contracts.rs @@ -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); +} diff --git a/v2/crates/wifi-densepose-physics/tests/deterministic_replay.rs b/v2/crates/wifi-densepose-physics/tests/deterministic_replay.rs new file mode 100644 index 00000000..74266f76 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/deterministic_replay.rs @@ -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() + ); +} diff --git a/v2/crates/wifi-densepose-physics/tests/dynamics.rs b/v2/crates/wifi-densepose-physics/tests/dynamics.rs new file mode 100644 index 00000000..f9c30d31 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/dynamics.rs @@ -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); +} diff --git a/v2/crates/wifi-densepose-physics/tests/floor_and_fall.rs b/v2/crates/wifi-densepose-physics/tests/floor_and_fall.rs new file mode 100644 index 00000000..05c9b784 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/floor_and_fall.rs @@ -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) + ); +} diff --git a/v2/crates/wifi-densepose-physics/tests/kinematics.rs b/v2/crates/wifi-densepose-physics/tests/kinematics.rs new file mode 100644 index 00000000..d0d1d09c --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/kinematics.rs @@ -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); +} diff --git a/v2/crates/wifi-densepose-physics/tests/learned.rs b/v2/crates/wifi-densepose-physics/tests/learned.rs new file mode 100644 index 00000000..99fbb22f --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/learned.rs @@ -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; + let device = NdArrayDevice::default(); + let model = ResidualGru::::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::::from_verified_artifact(&artifact, &device).unwrap(); + let input = Tensor::::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()); +} diff --git a/v2/crates/wifi-densepose-physics/tests/malformed_input.rs b/v2/crates/wifi-densepose-physics/tests/malformed_input.rs new file mode 100644 index 00000000..e1d24139 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/malformed_input.rs @@ -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) + ); +} diff --git a/v2/crates/wifi-densepose-physics/tests/properties.rs b/v2/crates/wifi-densepose-physics/tests/properties.rs new file mode 100644 index 00000000..e6faeacb --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/properties.rs @@ -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::(); + 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); +} diff --git a/v2/crates/wifi-densepose-physics/tests/schemas.rs b/v2/crates/wifi-densepose-physics/tests/schemas.rs new file mode 100644 index 00000000..5662de16 --- /dev/null +++ b/v2/crates/wifi-densepose-physics/tests/schemas.rs @@ -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}" + ); + } +} diff --git a/v2/crates/wifi-densepose-sensing-server/Cargo.toml b/v2/crates/wifi-densepose-sensing-server/Cargo.toml index b0f62cc0..bd99fd86 100644 --- a/v2/crates/wifi-densepose-sensing-server/Cargo.toml +++ b/v2/crates/wifi-densepose-sensing-server/Cargo.toml @@ -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" } diff --git a/v2/crates/wifi-densepose-sensing-server/src/lib.rs b/v2/crates/wifi-densepose-sensing-server/src/lib.rs index a2868464..36958830 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/lib.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/lib.rs @@ -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; diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 7911a70c..88d46090 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -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>` 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::>>() + .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 { let cls = &update.classification; if !cls.presence { @@ -4845,7 +5040,16 @@ async fn api_info(State(state): State) -> Json { })) } -async fn pose_current(State(state): State) -> Json { +#[derive(Debug, Default, Deserialize)] +struct PoseCurrentQuery { + #[serde(default)] + view: pose_physics::PoseView, +} + +async fn pose_current( + State(state): State, + Query(query): Query, +) -> 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) -> Json 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) -> 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) -> Json { @@ -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). diff --git a/v2/crates/wifi-densepose-sensing-server/src/pose_physics.rs b/v2/crates/wifi-densepose-sensing-server/src/pose_physics.rs new file mode 100644 index 00000000..864cb568 --- /dev/null +++ b/v2/crates/wifi-densepose-sensing-server/src/pose_physics.rs @@ -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 { + 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 { + 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, 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")); + } +}