mirror of
https://github.com/ruvnet/RuView
synced 2026-07-20 17:03:24 +00:00
17471e93ff
* feat(calibration): NodeGeometry transceiver-geometry recording (ADR-152 §2.1.1) PerceptAlign-motivated geometry capture at enrollment: per-node optional records (position, antenna orientation, inter-node distances, acquisition method) — recorded when known, never required. Event-sourced via EnrollmentEvent::GeometryRecorded (latest recording wins); persisted on SpecialistBank with serde defaults so pre-ADR-152 bank JSON loads cleanly (fixture-proven, and geometry-free banks serialize byte-shape-identical to the old schema); threaded through MultiNodeMixture as data only — the learned geometry embeddings and algorithmic fusion use are §2.1.2, deliberately deferred until the ADR-151 P6 LoRA heads exist. Geometry recorded from now on means banks captured today remain usable for layout-conditioned training later — you can't retroactively add geometry to data you didn't record. 8 new tests (3 geometry, 2 anchor, 2 bank, 1 multistatic) + full-loop extension (2-node geometry, one tape-measured + one unknown, surviving the bank JSON round-trip the runtime loads from). 50/50 calibration (both feature configs) + 23 CLI tests green. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(training): two-checkerboard camera↔room calibration for ADR-079 labels (ADR-152 §2.1.3) Defends the camera-supervised pipeline against PerceptAlign's "coordinate overfitting": MediaPipe keypoints were emitted in raw camera coordinates with no shared frame and no transceiver-geometry metadata — the exact label shape that memorizes deployment layout and collapses cross-layout. - scripts/calibrate-camera-room.py + calibration_lib.py: OpenCV two-checkerboard calibration → versioned bundle JSON (intrinsics, camera→room extrinsics, checkerboard spec, transceiver geometry, sha256 calibration_id). Intrinsics resolve from file > cache > multi-view computation > loud-warning 2-view fallback. - collect-ground-truth.py --calibration <bundle>: every sample gains keypoints_room (unit bearing rays from the camera center in the room frame — documented projective alignment; raw image coords preserved so training chooses), camera_origin_room, calibration_id, and the transceiver geometry stamp. Without the flag, output is byte-identical to before (tested) + a one-line ADR-152 warning. Design finding (recorded for ADR-152): a single planar checkerboard's corner grid is centrosymmetric — the reversed corner ordering fits a ghost camera pose with IDENTICAL reprojection error, so per-board flip disambiguation is mathematically ill-posed. solve_two_board_extrinsics solves the joint wall+floor set over all 4 flip combinations, where the minimum is unique — an independent reason the TWO-checkerboard method is required, beyond what PerceptAlign states. 15 headless pytest tests green (synthetic corners: extrinsics recovery incl. ghost resolution, bundle round-trip + hash stability, ray transforms w/ distortion + cross-resolution, no-calibration byte identity). Co-Authored-By: RuFlo <ruv@ruv.net> * feat(benchmarks): WiFlow-STD reproduction harness + measurement (a) results (ADR-152 §2.2) Shipped checkpoint REFUTED (0.08% PCK@20, wrong keypoint normalization); 6 reproducibility defects documented (broken imports, corrupted dataset tail with float32-max garbage that NaN-poisons fp16 BatchNorm, unreachable test phase). After repairs, retraining with upstream defaults reproduces 96.09% PCK@20 full-test / 96.61% corruption-free (published 97.25%) on RTX 5080. Claims graded MEASURED-EQUIVALENT; 2.23M params + ~0.055 GFLOPs verified. Third-party code/weights/data stay out of tree (gitignored). Co-Authored-By: claude-flow <ruv@ruv.net> * feat: ADR-152 Rust integrations + ADR-153 802.11bf protocol model - calibration: GeometryEmbedding — 32-slot permutation-invariant NodeGeometry featurization for future LoRA-head conditioning (ADR-152 §2.1.2); derived SpecialistBank::geometry_embedding() accessor; 59 tests - train: MaePretrainConfig + patchify/random-mask with UNSW measured recipe (80% masking, (30,3) patches; ADR-152 §2.3, arXiv 2511.18792); strict no-truncate/no-NaN policy; proptest properties - train: WiFlowStdModel — tch-gated port of the verified ~96%-PCK@20 WiFlow-STD architecture (ADR-152 §2.2 beyond-SOTA); ungated param formula pinned to 2,225,042; 15/17-keypoint support; 239 crate tests - hardware: ieee80211bf forward-compatibility protocol model (ADR-153): SpecProfile gates, SensingCapabilities negotiation, required ConsentMode, session FSM, SensingTransport + SimTransport + OpportunisticCsiBridge; full acceptance checklist covered; 156+4 tests - deps: ruvector bumps per ADR-152 §2.6 survey (mincut/solver 2.0.6, attention 2.1.0, gnn 2.2.0); vendor/ruvector synced to a083bd77f - docs: ADR-153 accepted; ADR-152 §2.2 status, §2.4 amendment, §2.6 added Workspace: 162 test suites green (--no-default-features); Python proof PASS. Known pre-existing flake: homecore-api env_empty_falls_back_to_defaults (unserialized env-var mutation) — untouched, follow-up. Co-Authored-By: claude-flow <ruv@ruv.net> * docs: CHANGELOG + CLAUDE.md entries for ADR-152 integrations and ADR-153 Co-Authored-By: claude-flow <ruv@ruv.net> * fix(train): repair tch-backend bit-rot — gated path compiles and tests run again Mechanical API refresh against current tch: Vec::from(Tensor) -> try_from (+ explicit flatten), numel() usize cast, Rem/div ops -> remainder() / divide_scalar_mode(floor) — the latter fixed a silent true-division bug in heatmap argmax decoding; clamp(1.0, f64::MAX) -> clamp_min (torch 2.x scalar overflow panic); petgraph EdgeRef import; missing EvalMetrics and verify_checkpoint_dir APIs that tests documented. wiflow_std roundtrip test uses safetensors (.pt _save_parameters roundtrip broken in torch 2.11 Windows). Gated: 349 passed (incl. all 20 wiflow_std); ungated: unchanged. Known pre-existing: gaussian-heatmap convention mismatch (2 tests), proof seed race under parallel threads — documented, deliberate follow-ups. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(train): WiFlow-STD PyTorch->tch weight import + numerical parity proof export_to_safetensors.py maps the retrained checkpoint (295 tensors -> 248 mapped, param sum exactly 2,225,042; num_batches_tracked dropped) into a tch-loadable safetensors plus a deterministic parity fixture. Gated #[ignore] integration test loads it strictly and asserts forward-pass agreement: max abs diff 1.192e-7 on the seed-42 fixture. dump_variable_names test makes the tch name layout authoritative. Zero architecture discrepancies found. Co-Authored-By: claude-flow <ruv@ruv.net> * fix: workflow-review findings — BN gamma init, ThresholdParams serde, init docs Concurrent validation workflow (2 review lanes + adversarial verification, 13 agents): 5 confirmed findings, 3 refuted. Fixes: - wiflow_std: pin BatchNorm gamma to 1.0 (tch default draws Uniform(0,1) — silently halves activations in from-scratch training; loaded checkpoints unaffected, parity re-verified after the change) - wiflow_std: document the conv-init divergences vs the reference's effective kaiming_normal(fan_out) re-init (from-scratch dynamics only) - ieee80211bf: ThresholdParams deserialization validates via try_from so the <=100 invariant holds for untrusted payloads (+ rejection test) Benchmarks (release, ruvzen): GeometryEmbedding 1.84us/call (542k/s), MAE tokenization 7.38us/window (135k/s), 802.11bf FSM 8.9M events/s — nothing suspicious. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr): ADR-152 §2.1.4 gate resolved — PerceptAlign repo MIT, dataset on HF Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): edge optimization measured + measurement (b) blocked + 92.9% retraction Edge optimization (ADR-152 optimize track): ONNX Runtime fp32 is the CPU latency win (3.2 ms/window, ~3.4x faster than torch, parity 2.4e-7); ORT dynamic int8 reaches 2.44 MB (paper's ~2.2 MB claim plausible only via conv-capable toolchains; -0.16pt PCK@20, +18% MPJPE, 2x slower); torch dynamic quant converts 0% of this conv-only model; fp16 halves storage free but is slower on CPU. Measurement (b) BLOCKED-ON-DATA: only 1,077 paired ESP32 windows exist (stop rule <2k). Forensic recheck of the surviving April holdout RETRACTS the ADR-079 '92.9% PCK@20' figure: constant-output model, absolute (not torso) threshold, 69 near-static frames — mean predictor scores 100% under that protocol; torso-PCK@20 is 19.1%. Corroborates PR #535. Stale citations removed from user-guide, readme-details, ADR-152 §2.1.3; no-citation rule extended to ADR-079 accuracy claims. Unblock: >=2k-window multi-pose paired session + torso-PCK re-baseline. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(user-guide): corrected camera-supervised collection tutorial Step 0 CSI-rate check + session-length math (window yield = frames/20 — the May session's 8x under-delivery was a ~12 Hz CSI rate, not an aligner bug); two-checkerboard calibration step (ADR-152 §2.1.3); pose-variety and confidence guidance; torso-normalized PCK + temporal-split + pred-variance eval protocol (lessons from the 92.9% retraction); scale presets re-keyed to realistic window counts. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): static PTQ int8 (calibrated) results + overnight capture script Conv-only static QDQ beats dynamic int8 on accuracy (PCK@20 96.61-96.63% vs 96.52%, MPJPE +10% vs +18% over fp32) at ~equal size/latency; all-ops QDQ strictly worse (int8 activations through attention glue). Entropy calibration verified bit-identical to MinMax on this data. Deployment: ONNX fp32 for speed (3.2ms), static conv-only QDQ for smallest (2.53MB). Also: scripts/overnight-empty-capture.py — segmented UDP CSI recorder for empty-room baselines (no glob collisions, detach-safe). Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): measurement (b) MEASURED — optimization transfer only, mean-pose baseline wins WiFlow-STD fine-tuned on 2,046 fresh single-room ESP32 paired windows (temporal 70/15/15, 70->540 adapter, K=17): pretrained-init 65% PCK@20 vs scratch 0% (optimization transfer) but frozen-trunk ~0% (no feature transfer), and NOTHING beats the mean-pose baseline (95.9% PCK@20 — single subject, near-static normalized coords). Honesty gates held: pred std 0.0113 (non-constant model) but mean-baseline dominance means no citable CSI->pose capability from this data. ADR-152 open question 1 answered partially; definitive answer needs multi-subject/position data. Two new aligner findings: heterogeneous csi_shape with silent zero-padding (~20%), and extractCsiMatrix's transposed shape label (frame-major data, [nSc, nFrames] label) — fixes pending. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): efficiency sweep MEASURED — half model dominates full reference Compact WiFlow-STD variants on the same data/split/protocol: half (843,834 params, 0.38x) strictly dominates the 2.23M reference (PCK@20 96.62 vs 96.61, PCK@50 99.47 vs 99.11, MPJPE 0.00898 vs 0.0094) — the published architecture is over-parameterized for its own benchmark. quarter (338k) 96.05%; tiny (56,290 params, 1/39.5) holds 94.11% — a ~220KB fp32 edge candidate. In-domain caveats recorded; cross-domain untested. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(train): compact WiFlow-STD presets in Rust + tiny edge artifact (ADR-152) WiFlowStdConfig gains half()/quarter()/tiny() mirroring the overnight sweep exactly: TcnGroupsMode (Fixed/Gcd/Depthwise), input_pw_groups, derived stride schedule and decoder-mid (all default to upstream behavior; legacy serde JSON unaffected). Param formulas pin to trained ground truth first try: 843,834 / 338,600 / 56,290; default 2,225,042 pin and 1.192e-7 parity unchanged. 248 tests green. Tiny edge artifact (tiny_edge_bench.py): ONNX fp32 = 295 KB, 0.66 ms/win (~1,500/s CPU), 94.11% PCK@20 (matches sweep clean-test exactly; parity 1.49e-7). Static int8 is a bad trade at this scale (-1.43pt, +19% MPJPE, -16% size, slower) — recorded as negative result. Export note: width-16 breaks AdaptiveAvgPool((15,1)) TorchScript export; replaced by exact mean+matmul equivalent, proven by parity. Co-Authored-By: claude-flow <ruv@ruv.net> * fix: resolve all 10 confirmed code-review findings (7-angle review, 20/20 verified) wiflow_std: min_feature_width (default 15) replaces the keypoints->stride coupling — for_keypoints(17) now provably builds the trained [2,2,2,2] graph and pools 15->17, matching the validated Python protocol (pinned by tests); param_count() total on invalid configs; random_mask returns Result and rejects non-finite/out-of-range ratios; trainer checkpoints switched to safetensors (.pt VarStore roundtrip broken on Windows torch 2.11). ieee80211bf: SBP proxy now re-triggers instances and relays reports via Action::RelaySbpReport -> SensingFrame::SbpReport (clients consume via their existing path); missed_instances reset on success = consecutive semantics; SessionTable gains a guarded SBP entry point + unknown-id drop counter; initiator-role sessions reject inbound setup/SBP requests (RejectedNotSupported) closing the idle hijack; StartSetup/StartSbp outside Idle return InvalidStateForCommand; SBP validation unified through evaluate_setup with a 1:1 SetupStatus->SbpStatus mapping. events.rs split out to honor the 500-line cap. calibration/cli: enrollment geometry now actually reaches trained banks — both production call sites attach .with_geometry; --geometry flag on train-room and POST /enroll/geometry + train-body geometry on calibrate-serve give production a recording surface; geometry-free banks log the ADR-152 §2.1.2 note. benchmarks: corruption masks committed as ground truth (unregenerable after in-place cleaning; verified bit-identical regeneration from the pristine copy) + generate_corruption_masks.py producer; _bench_common.py dedups the 5x-copied shim/evaluate/seed/remap (post-refactor PCK@20 re-verified equal to the last digit); remote scripts get the mmap patch; tiny_edge --calib validated multiple-of-64; onnx_bench --help no longer executes (and overwrote) the export — artifact restored byte-exact. Workspace: 2,963 tests passed, 0 failed; Python proof PASS. Co-Authored-By: claude-flow <ruv@ruv.net> * ci: build workspace tests without debuginfo — runner disk exhaustion The combined 38-crate debug target exceeds the GitHub runner's disk ('final link failed: No space left on device'); the same tree measured 151GB locally with full debuginfo. CARGO_PROFILE_{DEV,TEST}_DEBUG=0 shrinks the target ~5-10x; debuginfo serves no purpose in CI test runs. Co-Authored-By: claude-flow <ruv@ruv.net>
319 lines
10 KiB
Rust
319 lines
10 KiB
Rust
//! Integration + property tests for [`wifi_densepose_train::mae`]
|
||
//! (ADR-152 §2.3 — UNSW MAE pretraining recipe).
|
||
//!
|
||
//! All deterministic tests use fixed seeds; property tests use `proptest`
|
||
//! with its default deterministic-replay machinery.
|
||
|
||
use proptest::prelude::*;
|
||
use wifi_densepose_train::mae::{
|
||
patchify, random_mask, unpatchify, unpatchify_visible, MaePretrainConfig,
|
||
};
|
||
use wifi_densepose_train::MaeError;
|
||
|
||
/// Deterministic test window: value = t * 1000 + sc (every cell unique).
|
||
fn window(time: usize, subc: usize) -> Vec<f32> {
|
||
(0..time * subc)
|
||
.map(|i| ((i / subc) * 1000 + i % subc) as f32)
|
||
.collect()
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Config defaults + validation
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn default_config_matches_unsw_recipe() {
|
||
let cfg = MaePretrainConfig::default();
|
||
assert!((cfg.mask_ratio - 0.80).abs() < 1e-12);
|
||
assert_eq!(cfg.patch_time, 30);
|
||
assert_eq!(cfg.patch_subc, 3);
|
||
assert_eq!(cfg.seed, 42);
|
||
cfg.validate().expect("default recipe is valid");
|
||
}
|
||
|
||
#[test]
|
||
fn config_json_round_trip() {
|
||
let cfg = MaePretrainConfig::default();
|
||
let json = serde_json::to_string(&cfg).unwrap();
|
||
let back: MaePretrainConfig = serde_json::from_str(&json).unwrap();
|
||
assert_eq!(back, cfg);
|
||
}
|
||
|
||
#[test]
|
||
fn invalid_mask_ratio_rejected() {
|
||
for ratio in [0.0, 1.0, -0.1, 1.5, f64::NAN] {
|
||
let cfg = MaePretrainConfig {
|
||
mask_ratio: ratio,
|
||
..MaePretrainConfig::default()
|
||
};
|
||
assert!(cfg.validate().is_err(), "ratio {ratio} should be invalid");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn zero_patch_dims_rejected() {
|
||
let cfg = MaePretrainConfig {
|
||
patch_time: 0,
|
||
..MaePretrainConfig::default()
|
||
};
|
||
assert!(cfg.validate().is_err());
|
||
let cfg = MaePretrainConfig {
|
||
patch_subc: 0,
|
||
..MaePretrainConfig::default()
|
||
};
|
||
assert!(cfg.validate().is_err());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Divisibility policy: error, never truncate
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn non_divisible_window_errors_with_crop_hint() {
|
||
let cfg = MaePretrainConfig::default(); // (30, 3)
|
||
// Default TrainingConfig window 100 × 56 is NOT divisible by (30, 3).
|
||
let err = cfg.validate_for_window(100, 56).unwrap_err();
|
||
match err {
|
||
MaeError::NotDivisible {
|
||
axis,
|
||
window,
|
||
patch,
|
||
remainder,
|
||
crop,
|
||
} => {
|
||
assert_eq!(axis, "time");
|
||
assert_eq!(window, 100);
|
||
assert_eq!(patch, 30);
|
||
assert_eq!(remainder, 10);
|
||
assert_eq!(crop, 90);
|
||
}
|
||
other => panic!("expected NotDivisible, got {other:?}"),
|
||
}
|
||
assert_eq!(cfg.cropped_window_shape(100, 56), (90, 54));
|
||
// The hinted crop validates cleanly.
|
||
cfg.validate_for_window(90, 54).expect("crop is divisible");
|
||
assert_eq!(cfg.num_patches(90, 54).unwrap(), 3 * 18);
|
||
}
|
||
|
||
#[test]
|
||
fn patch_larger_than_window_errors() {
|
||
let cfg = MaePretrainConfig::default();
|
||
let err = cfg.validate_for_window(20, 3).unwrap_err();
|
||
assert!(matches!(
|
||
err,
|
||
MaeError::PatchExceedsWindow { axis: "time", .. }
|
||
));
|
||
}
|
||
|
||
#[test]
|
||
fn window_length_mismatch_errors() {
|
||
let cfg = MaePretrainConfig::default();
|
||
let buf = vec![0.0_f32; 89 * 54]; // declared 90 × 54
|
||
let err = patchify(&buf, 90, 54, &cfg).unwrap_err();
|
||
assert!(matches!(err, MaeError::WindowShapeMismatch { .. }));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// NaN handling
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn nan_and_inf_input_rejected_with_location() {
|
||
let cfg = MaePretrainConfig::default();
|
||
let mut buf = window(90, 54);
|
||
buf[2 * 54 + 7] = f32::NAN;
|
||
match patchify(&buf, 90, 54, &cfg).unwrap_err() {
|
||
MaeError::NonFiniteValue { row, col, .. } => {
|
||
assert_eq!((row, col), (2, 7));
|
||
}
|
||
other => panic!("expected NonFiniteValue, got {other:?}"),
|
||
}
|
||
buf[2 * 54 + 7] = f32::INFINITY;
|
||
assert!(matches!(
|
||
patchify(&buf, 90, 54, &cfg),
|
||
Err(MaeError::NonFiniteValue { .. })
|
||
));
|
||
}
|
||
|
||
#[test]
|
||
fn finite_input_is_nan_free_after_round_trip() {
|
||
let cfg = MaePretrainConfig::default();
|
||
let buf = window(90, 54);
|
||
let grid = patchify(&buf, 90, 54, &cfg).unwrap();
|
||
assert!(grid.patches.iter().flatten().all(|v| v.is_finite()));
|
||
assert!(unpatchify(&grid).iter().all(|v| v.is_finite()));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Patchify / unpatchify round trip
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn patchify_unpatchify_identity_default_recipe() {
|
||
let cfg = MaePretrainConfig::default();
|
||
let buf = window(90, 54);
|
||
let grid = patchify(&buf, 90, 54, &cfg).unwrap();
|
||
assert_eq!(grid.n_patches(), 54);
|
||
assert_eq!(grid.patch_len(), 90);
|
||
assert_eq!(grid.window_shape(), (90, 54));
|
||
assert_eq!(unpatchify(&grid), buf);
|
||
}
|
||
|
||
#[test]
|
||
fn patch_layout_is_time_major() {
|
||
// 4 × 4 window, (2, 2) patches → patch 0 is rows 0–1 × cols 0–1.
|
||
let cfg = MaePretrainConfig {
|
||
patch_time: 2,
|
||
patch_subc: 2,
|
||
..MaePretrainConfig::default()
|
||
};
|
||
let buf = window(4, 4);
|
||
let grid = patchify(&buf, 4, 4, &cfg).unwrap();
|
||
assert_eq!(grid.patches[0], vec![0.0, 1.0, 1000.0, 1001.0]);
|
||
// Patch index 1 is the next subcarrier block on the same time rows.
|
||
assert_eq!(grid.patches[1], vec![2.0, 3.0, 1002.0, 1003.0]);
|
||
// Patch index n_patches_subc starts the second time row of patches.
|
||
assert_eq!(grid.patches[2], vec![2000.0, 2001.0, 3000.0, 3001.0]);
|
||
}
|
||
|
||
#[test]
|
||
fn unpatchify_visible_restores_visible_and_fills_masked() {
|
||
let cfg = MaePretrainConfig::default();
|
||
let buf = window(90, 54);
|
||
let (grid, mask) = cfg.mask_window(&buf, 90, 54).unwrap();
|
||
let fill = -1.0_f32;
|
||
let recon = unpatchify_visible(&grid, &mask.visible, fill);
|
||
|
||
// Visible patch regions are identical to the input; masked regions = fill.
|
||
let full = unpatchify(&grid);
|
||
assert_eq!(full, buf);
|
||
let mut n_fill = 0usize;
|
||
for (i, (&r, &orig)) in recon.iter().zip(buf.iter()).enumerate() {
|
||
if r == fill && orig != fill {
|
||
n_fill += 1;
|
||
} else {
|
||
assert_eq!(r, orig, "visible value at flat index {i} must round-trip");
|
||
}
|
||
}
|
||
assert_eq!(n_fill, mask.masked.len() * grid.patch_len());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Random mask: exact count, determinism, disjointness
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn mask_count_is_exact_for_default_recipe() {
|
||
// 54 patches @ 0.80 → round(43.2) = 43 masked, 11 visible.
|
||
let cfg = MaePretrainConfig::default();
|
||
assert_eq!(cfg.num_masked(54), 43);
|
||
let mask = random_mask(54, cfg.mask_ratio, cfg.seed).unwrap();
|
||
assert_eq!(mask.masked.len(), 43);
|
||
assert_eq!(mask.visible.len(), 11);
|
||
}
|
||
|
||
#[test]
|
||
fn same_seed_same_mask_different_seed_differs() {
|
||
let a = random_mask(100, 0.80, 7).unwrap();
|
||
let b = random_mask(100, 0.80, 7).unwrap();
|
||
assert_eq!(a, b, "same (n, ratio, seed) must reproduce the mask");
|
||
|
||
let c = random_mask(100, 0.80, 8).unwrap();
|
||
assert_ne!(a.masked, c.masked, "different seeds must differ");
|
||
}
|
||
|
||
#[test]
|
||
fn random_mask_rejects_invalid_ratios() {
|
||
// Error-not-silent: NaN must not silently mask 0 patches; ratios outside
|
||
// (0, 1) must not degenerate to all-visible / all-masked grids.
|
||
for ratio in [
|
||
f64::NAN,
|
||
f64::INFINITY,
|
||
f64::NEG_INFINITY,
|
||
1.0,
|
||
1.5,
|
||
0.0,
|
||
-0.1,
|
||
] {
|
||
let err = random_mask(54, ratio, 42).unwrap_err();
|
||
assert!(
|
||
matches!(err, MaeError::InvalidMaskRatio { .. }),
|
||
"ratio {ratio} must be rejected, got {err:?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn mask_window_rejects_invalid_ratio_before_masking() {
|
||
let cfg = MaePretrainConfig {
|
||
mask_ratio: f64::NAN,
|
||
..MaePretrainConfig::default()
|
||
};
|
||
let buf = window(90, 54);
|
||
assert!(matches!(
|
||
cfg.mask_window(&buf, 90, 54),
|
||
Err(MaeError::InvalidMaskRatio { .. })
|
||
));
|
||
}
|
||
|
||
proptest! {
|
||
/// Exact count, sortedness, range, disjointness, and full coverage hold
|
||
/// for arbitrary grid sizes, ratios, and seeds.
|
||
#[test]
|
||
fn prop_mask_invariants(
|
||
n in 1usize..600,
|
||
ratio in 0.01f64..0.99,
|
||
seed in any::<u64>(),
|
||
) {
|
||
let mask = random_mask(n, ratio, seed).unwrap();
|
||
let expected_masked = ((ratio * n as f64).round() as usize).min(n);
|
||
prop_assert_eq!(mask.masked.len(), expected_masked);
|
||
prop_assert_eq!(mask.masked.len() + mask.visible.len(), n);
|
||
|
||
// In range, sorted, strictly increasing (no duplicates).
|
||
for set in [&mask.masked, &mask.visible] {
|
||
for w in set.windows(2) {
|
||
prop_assert!(w[0] < w[1]);
|
||
}
|
||
if let Some(&last) = set.last() {
|
||
prop_assert!(last < n);
|
||
}
|
||
}
|
||
// Disjoint + complete: merged sets are exactly 0..n.
|
||
let mut all: Vec<usize> = mask.masked.iter().chain(&mask.visible).copied().collect();
|
||
all.sort_unstable();
|
||
prop_assert_eq!(all, (0..n).collect::<Vec<_>>());
|
||
}
|
||
|
||
/// Determinism by seed for arbitrary inputs.
|
||
#[test]
|
||
fn prop_mask_deterministic(n in 1usize..400, seed in any::<u64>()) {
|
||
prop_assert_eq!(
|
||
random_mask(n, 0.80, seed).unwrap(),
|
||
random_mask(n, 0.80, seed).unwrap()
|
||
);
|
||
}
|
||
|
||
/// Round-trip identity for arbitrary divisible window/patch geometries.
|
||
#[test]
|
||
fn prop_patchify_round_trip(
|
||
pt in 1usize..8,
|
||
ps in 1usize..8,
|
||
nt in 1usize..6,
|
||
ns in 1usize..6,
|
||
seed in any::<u64>(),
|
||
) {
|
||
let (time, subc) = (pt * nt, ps * ns);
|
||
let cfg = MaePretrainConfig {
|
||
patch_time: pt,
|
||
patch_subc: ps,
|
||
seed,
|
||
..MaePretrainConfig::default()
|
||
};
|
||
let buf = window(time, subc);
|
||
let grid = patchify(&buf, time, subc, &cfg).unwrap();
|
||
prop_assert_eq!(grid.n_patches(), nt * ns);
|
||
prop_assert_eq!(unpatchify(&grid), buf);
|
||
}
|
||
}
|