mirror of
https://github.com/ruvnet/RuView
synced 2026-07-20 17:03:24 +00:00
feat(ruview-gamma): centralize claim-gate invariant + add HIL acceptance contract
Two of the "what to optimize next" items, as enforced code: 1. Hard claim gate: extract the release rule into a single acceptance::claim_allowed(entrainment, safety, adherence, repeatability) = strict AND of all four, used by every path. A test proves every 3-of-4 subset is denied, so no path can silently weaken the gate to an OR/subset. 2. Hardware-in-the-loop contract (new hil module): verify_hil grades a captured actuator bench measurement against fixed targets — LED frequency +/-0.1 Hz, audio-visual sync < 5 ms, stop-signal -> actuator-off < 100 ms, session-hash reproducibility 100%, EEG entrainment lift >= 20% over fixed 40 Hz. Every failure mode fails closed (missing/NaN stop measurement, no replay, any hash mismatch). The crate stays a deterministic leaf: firmware records the measurements, this module grades them. README: benchmark table (safety tick ~8 ns, recommend ~15 us, cohort kNN/500 ~15 us, calibration ~115 us, acceptance grading ~425 us enrollment-only) and the positioning line — "a governed personalization engine that refuses to overpromise." 9 new tests; crate now 97 + 1 doctest; pinned witness 13cb164c... preserved. Workspace gate: 2,898 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
This commit is contained in:
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **`ruview-gamma` claim-gate invariant + hardware-in-the-loop contract.** Centralized the claim release rule into a single `acceptance::claim_allowed(entrainment, safety, adherence, repeatability)` (strict AND of all four) used by every path, with a test proving every 3-of-4 subset is denied — no path can weaken the gate. New `hil` module: `verify_hil` grades a captured actuator bench measurement against fixed targets (LED frequency ±0.1 Hz, audio-visual sync < 5 ms, stop-signal→actuator-off < 100 ms, session-hash reproducibility 100%, EEG entrainment lift ≥ 20% over fixed 40 Hz) — the next acceptance milestone for a real LED+speaker (e.g. ESP32) actuator; all failure modes fail closed (missing stop measurement, no replay, any hash mismatch). README gains the benchmark table and the "governed personalization engine that refuses to overpromise" positioning. 9 new tests; crate now 97 + 1 doctest; pinned witness preserved.
|
||||
- **`ruview-gamma` generalized to an adaptive sensory neuromodulation platform (ADR-250 §23).** 40 Hz is now one prior in one program, not the product. New `program` module: `NeuroProgram` catalog of 7 use cases (Alzheimer's research, post-stroke cognition, sleep optimization, attention/working-memory, mood/arousal, home wellness, drug+device trial infrastructure), each with its own `SafetyEnvelope`, starting prior, `ObjectiveWeights`, physiological-state gating (sleep permits `Asleep` + near-dark brightness cap; attention requires wakefulness), `EvidenceLevel`, and a single non-disease claim. New `acceptance` module makes the acceptance sentence executable: `AcceptanceHarness` grades a program over ≥3 repeats on entrainment gain, safety-stop rate, adherence, and optimal-frequency repeatability, exposing a `ClaimGate` that returns the program's claim **only if all four criteria pass** — the marketing claim is otherwise unreadable (`NO_CLAIM`). Governor wiring: `enroll_program` (per-program envelope/objective; `enroll` stays the bare Alzheimer's-defaults path so the pinned witness `13cb164c…` is preserved), `program()`, `prior()`, `state_eligible()`. 13 new module tests + 2 platform integration tests (per-program envelope enforced end-to-end — a stimulus valid for Alzheimer's is refused by the sleep program; acceptance gates every catalog program's claim); crate now 88 tests + 1 doctest. Bench: full 3-repeat program grading ~425 µs.
|
||||
- **`ruview-gamma` RuVector self-learning layer (ADR-250 §10 items 3–6).** New `ruvector` module: anonymized `ProfileStore` (one-way SHA-256 hashed tags, never `person_id`; safe-session scores only), deterministic exact kNN (fixed-range normalization, index tie-break), **cohort warm-start** — a new person's optimizer is seeded from the k nearest responders as down-weighted GP pseudo-observations (`BayesianOptimizer::observe_prior`, ≥25× real-observation noise, excluded from the EI incumbent / audit / clinician report), **physiological drift detection** (Welford centroid with stimulus-input fields masked out of the distance; `Drifted` recommends re-calibration), and deterministic k-means response clustering (farthest-point seeding, no RNG). Wired into `RufloGovernor` (`seed_from_cohort`, `export_anonymized_profile`, per-session `drift_status`). The GP gains per-observation noise (real path unchanged — pinned witness `13cb164c…` preserved). 11 new module tests + 2 integration tests (cohort warm-start beats the cold 40 Hz prior for a detuned subject; collapsed physiology flags drift); crate now 75 tests + 1 doctest. Benches: kNN over 500 profiles ~15 µs, full warm-start ~16 µs; no regression on existing paths.
|
||||
- **`ruview-gamma` crate (ADR-250) — Adaptive Gamma Entrainment.** Governed, deterministic, safety-constrained personalization of 40 Hz-prior light+sound stimulation, treating 40 Hz as the evidence-based *starting prior* and learning each person's safe entrainment response curve. Eleven modules: `stimulus` (params + `SafetyEnvelope` validate/clamp), `safety` (exclusion screen + latched `SafetyMonitor` with hard-stop reasons), `response` (`RuViewState`, optional `EegMeasurement`, 20-field `PersonResponseVector` with sticky adverse flag), `objective` (safe-entrainment score; safety is a hard gate, not a weight), `simulator` (deterministic ChaCha20 `frequency_response_curve`), `optimizer` (Phase-1 calibration sweep + Phase-2 GP/Expected-Improvement + Phase-4 closed-loop control), `bandit` (Phase-3 LinUCB over envelope-safe arms), `session` (reproducible SHA-256 `session_hash`), `ruflo` (consent→exclusion→envelope→run→monitor→score→update→witnessed audit, trial/sham mode, clinician export, claim discipline), `proof` (deterministic bundle witness), `math` (dependency-light numerics). **Safety invariant** (asserted in tests): no recommendation, calibration step, bandit arm, or closed-loop nudge can ever emit a stimulus outside the `SafetyEnvelope`; non-finite inputs clamp to the conservative floor. **Claim discipline**: the only product claim is `PRODUCT_CLAIM` = "personalized entrainment optimization" — never Alzheimer's treatment (ADR-250 §19). Standalone leaf crate (no internal RuView deps), `publish = false` pending safety sign-off. 64 unit/integration tests + 1 doctest pass; deterministic witness pinned (`13cb164c…`); criterion benches (safety-stop tick ~9.3 ns vs the ADR §17 500 ms bound, Bayesian recommend ~105 µs, full 9-session governed sweep ~486 µs). See [ADR-250](docs/adr/ADR-250-adaptive-gamma-entrainment.md).
|
||||
|
||||
@@ -1,4 +1,65 @@
|
||||
# ruview-gamma — Adaptive Gamma Entrainment (ADR-250)
|
||||
# ruview-gamma — Adaptive Sensory Neuromodulation (ADR-250)
|
||||
|
||||
> **The most valuable thing here is not 40 Hz. It is a governed personalization
|
||||
> engine that refuses to overpromise.**
|
||||
|
||||
The control brain for an adaptive light-and-sound neuromodulation device. The
|
||||
device plays stimulation; **RuView** reads the body as feedback; **RuVector**
|
||||
learns the personal response map; **RuFlo** governs the safety, audit trail, and
|
||||
claim boundary. The breakthrough is not speed alone — it is **safe adaptive
|
||||
personalization with proof discipline**.
|
||||
|
||||
It starts from 40 Hz as the research prior, then learns whether a person
|
||||
responds better at 38.5, 40, 41.2, or another safe setting — watching breathing,
|
||||
stillness, restlessness, adherence, and sensor confidence. If something goes
|
||||
wrong, the session locks. If a program has not proven entrainment, safety,
|
||||
adherence, and repeatability, it cannot advertise a benefit — it returns
|
||||
*research use only*.
|
||||
|
||||
## Benchmarks (this container — indicative)
|
||||
|
||||
| Path | Current | Role |
|
||||
|------|---------|------|
|
||||
| Safety tick | ~8 ns | real-time stop path |
|
||||
| Recommendation | ~15 µs | per-session decision |
|
||||
| Cohort kNN (500 profiles) | ~15 µs | warm-start matching |
|
||||
| Calibration sweep | ~115 µs | setup and tuning |
|
||||
| Full acceptance grading | ~425 µs | enrollment-only (offline) |
|
||||
|
||||
The per-session control loop is microseconds; the heavier acceptance grading is
|
||||
enrollment-time work, not on the loop. No regression across the optimization
|
||||
passes.
|
||||
|
||||
## The hard claim gate
|
||||
|
||||
A program's benefit claim is releasable through exactly one invariant
|
||||
(`acceptance::claim_allowed`), used everywhere:
|
||||
|
||||
```text
|
||||
claim_allowed = entrainment_pass AND safety_pass
|
||||
AND adherence_pass AND repeatability_pass
|
||||
```
|
||||
|
||||
Anything short of all four returns `research use only — … no claim`
|
||||
(`acceptance::NO_CLAIM`). The marketing claim is unreadable except through the
|
||||
gate.
|
||||
|
||||
## Next milestone — hardware in the loop (`hil`)
|
||||
|
||||
The software core is proven against a deterministic simulator; the next
|
||||
acceptance bar is a real LED + speaker actuator (e.g. ESP32-driven) plus the
|
||||
stop path. `hil::verify_hil` grades a captured bench measurement against fixed
|
||||
targets:
|
||||
|
||||
| Test | Target |
|
||||
|------|--------|
|
||||
| LED frequency accuracy | ±0.1 Hz |
|
||||
| Audio-visual sync drift | < 5 ms |
|
||||
| Stop signal → actuator off | < 100 ms |
|
||||
| Session-hash reproducibility | 100% |
|
||||
| EEG entrainment lift vs fixed 40 Hz | ≥ 20% |
|
||||
|
||||
---
|
||||
|
||||
Governed, deterministic, **safety-constrained** personalization of 40 Hz-prior
|
||||
multisensory (light + sound) stimulation. Treats 40 Hz as the evidence-based
|
||||
@@ -44,7 +105,8 @@ conservative floor, never the cap.
|
||||
| `bandit` | §8 P3 | LinUCB contextual bandit over envelope-safe arms |
|
||||
| `ruvector` | §10 items 3–6 | anonymized `ProfileStore` (one-way hashed tags), deterministic kNN, cohort warm-start priors (down-weighted pseudo-observations), `DriftDetector` over the physiological sub-vector, deterministic k-means clustering |
|
||||
| `program` | §23 | `NeuroProgram` catalog (7 use cases) — per-program envelope, prior, objective, state-gating, evidence level, and gated claim |
|
||||
| `acceptance` | §18/§23.1 | `AcceptanceHarness` + `ClaimGate` — entrainment/safety/adherence/repeatability gate; a program's claim is unreadable until all four pass |
|
||||
| `acceptance` | §18/§23.1 | `AcceptanceHarness` + `ClaimGate` + the `claim_allowed` invariant — entrainment/safety/adherence/repeatability gate; a program's claim is unreadable until all four pass |
|
||||
| `hil` | §17/§21 M2 | hardware-in-the-loop contract: `verify_hil` grades a captured actuator measurement (LED ±0.1 Hz, A/V sync < 5 ms, stop < 100 ms, hash 100%, EEG lift ≥ 20%) |
|
||||
| `session` | §11, §13 | hashable `SessionRecord`, reproducible `session_hash` (SHA-256, quantized canonical form) |
|
||||
| `ruflo` | §11 | consent → exclusion → envelope → run → monitor → score → update → witnessed audit; trial/sham mode; clinician export; claim discipline |
|
||||
| `proof` | — | deterministic bundle witness (mirrors `nvsim` / `verify.py`) |
|
||||
|
||||
@@ -20,6 +20,26 @@ use crate::stimulus::StimulusParameters;
|
||||
/// claim, no disease claim — only a statement that evidence is insufficient.
|
||||
pub const NO_CLAIM: &str = "research use only — acceptance criteria not yet met; no claim";
|
||||
|
||||
/// **The hard claim-gate invariant** (ADR-250 §23.1). The single source of
|
||||
/// truth used everywhere a claim could be released:
|
||||
///
|
||||
/// ```text
|
||||
/// claim_allowed = entrainment_pass AND safety_pass
|
||||
/// AND adherence_pass AND repeatability_pass
|
||||
/// ```
|
||||
///
|
||||
/// Anything short of all four returns the research-only string. Centralizing it
|
||||
/// here means no path can accidentally weaken the gate to an OR or a subset.
|
||||
#[inline]
|
||||
pub fn claim_allowed(
|
||||
entrainment_pass: bool,
|
||||
safety_pass: bool,
|
||||
adherence_pass: bool,
|
||||
repeatability_pass: bool,
|
||||
) -> bool {
|
||||
entrainment_pass && safety_pass && adherence_pass && repeatability_pass
|
||||
}
|
||||
|
||||
/// Thresholds a program must clear (ADR-250 §18 generalized). Defaults mirror
|
||||
/// the ADR's published targets; programs may tighten them.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -183,7 +203,8 @@ impl AcceptanceHarness {
|
||||
let safety_pass = safety_stop_rate <= c.max_safety_stop_rate;
|
||||
let adherence_pass = mean_adherence >= c.min_adherence;
|
||||
let repeatability_pass = repeatability_band_hz <= c.max_repeatability_band_hz;
|
||||
let overall_pass = entrainment_pass && safety_pass && adherence_pass && repeatability_pass;
|
||||
let overall_pass =
|
||||
claim_allowed(entrainment_pass, safety_pass, adherence_pass, repeatability_pass);
|
||||
|
||||
AcceptanceReport {
|
||||
program_id: program.id.to_string(),
|
||||
@@ -243,6 +264,24 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::response::{RuViewState, SleepState};
|
||||
|
||||
#[test]
|
||||
fn claim_allowed_requires_all_four_and_rejects_every_subset() {
|
||||
// All four → allowed.
|
||||
assert!(claim_allowed(true, true, true, true));
|
||||
// Every 3-of-4 subset (one false) → denied. This is the AND, not OR,
|
||||
// guarantee the whole gate rests on.
|
||||
let one_false = [
|
||||
(false, true, true, true),
|
||||
(true, false, true, true),
|
||||
(true, true, false, true),
|
||||
(true, true, true, false),
|
||||
];
|
||||
for (e, s, a, r) in one_false {
|
||||
assert!(!claim_allowed(e, s, a, r), "subset {e}{s}{a}{r} must be denied");
|
||||
}
|
||||
assert!(!claim_allowed(false, false, false, false));
|
||||
}
|
||||
|
||||
fn detuned_subject() -> (String, LatentPerson) {
|
||||
// A subject whose latent peak is clearly off the prior frequency, so an
|
||||
// adaptive program has real gain to find.
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Hardware-in-the-loop (HIL) acceptance contract — Milestone "device harness"
|
||||
//! (ADR-250 §17, §21 M2).
|
||||
//!
|
||||
//! The software core is proven against the deterministic simulator; the next
|
||||
//! acceptance milestone is **real hardware**: an LED + speaker actuator (e.g.
|
||||
//! driven by an ESP32) plus the safety stop path. This module defines the
|
||||
//! *contract* every actuator integration must satisfy and a verifier that
|
||||
//! grades a captured [`HilMeasurement`] against fixed targets. It does **not**
|
||||
//! talk to hardware — the firmware/driver records the measurements and submits
|
||||
//! them here, keeping this crate a deterministic, dependency-light leaf.
|
||||
//!
|
||||
//! | Test | Target |
|
||||
//! |------|--------|
|
||||
//! | LED frequency accuracy | ±0.1 Hz |
|
||||
//! | Audio-visual sync drift | < 5 ms |
|
||||
//! | Stop signal → actuator off | < 100 ms |
|
||||
//! | Session-hash reproducibility | 100% |
|
||||
//! | EEG entrainment lift vs fixed 40 Hz | ≥ 20% |
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Fixed HIL targets (ADR-250 §17 acceptance + §18). Constants, not config:
|
||||
/// these are the bar a device must clear to be called validated.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct HilTargets {
|
||||
/// Max |measured − commanded| LED/flicker frequency, Hz.
|
||||
pub max_frequency_error_hz: f64,
|
||||
/// Max audio-visual onset skew, milliseconds.
|
||||
pub max_av_sync_drift_ms: f64,
|
||||
/// Max latency from stop assertion to actuator-off, milliseconds.
|
||||
pub max_stop_latency_ms: f64,
|
||||
/// Min fraction of replayed sessions whose witness hash reproduced.
|
||||
pub min_hash_reproducibility: f64,
|
||||
/// Min EEG entrainment lift over fixed 40 Hz, as a fraction.
|
||||
pub min_eeg_lift: f64,
|
||||
}
|
||||
|
||||
impl Default for HilTargets {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_frequency_error_hz: 0.1,
|
||||
max_av_sync_drift_ms: 5.0,
|
||||
max_stop_latency_ms: 100.0,
|
||||
min_hash_reproducibility: 1.0,
|
||||
min_eeg_lift: 0.20,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A captured bench measurement from a real actuator run. Populated by the
|
||||
/// firmware/driver test harness (e.g. measuring LED frequency with a photodiode
|
||||
/// and a logic analyzer, sync with a dual-channel capture, stop latency from
|
||||
/// GPIO assert to PWM-off).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct HilMeasurement {
|
||||
/// Commanded frequency the controller asked for (Hz).
|
||||
pub commanded_frequency_hz: f64,
|
||||
/// Frequency actually measured at the LED (Hz).
|
||||
pub measured_frequency_hz: f64,
|
||||
/// Measured audio-visual onset skew (ms).
|
||||
pub av_sync_drift_ms: f64,
|
||||
/// Measured stop-assert → actuator-off latency (ms).
|
||||
pub stop_latency_ms: f64,
|
||||
/// Replayed sessions whose witness hash matched / total replayed.
|
||||
pub hashes_reproduced: u32,
|
||||
pub hashes_total: u32,
|
||||
/// Mean EEG entrainment under the adaptive protocol.
|
||||
pub eeg_entrainment_adaptive: f64,
|
||||
/// Mean EEG entrainment under fixed 40 Hz (the control arm).
|
||||
pub eeg_entrainment_fixed_40hz: f64,
|
||||
}
|
||||
|
||||
/// Per-criterion verdict for a HIL run.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct HilReport {
|
||||
pub frequency_error_hz: f64,
|
||||
pub frequency_pass: bool,
|
||||
pub av_sync_pass: bool,
|
||||
pub stop_latency_pass: bool,
|
||||
pub hash_reproducibility: f64,
|
||||
pub hash_pass: bool,
|
||||
pub eeg_lift: f64,
|
||||
pub eeg_lift_pass: bool,
|
||||
/// True only if every HIL criterion passes — the device is bench-validated.
|
||||
pub overall_pass: bool,
|
||||
}
|
||||
|
||||
/// Grade a [`HilMeasurement`] against [`HilTargets`].
|
||||
pub fn verify_hil(m: &HilMeasurement, t: &HilTargets) -> HilReport {
|
||||
let frequency_error_hz = (m.measured_frequency_hz - m.commanded_frequency_hz).abs();
|
||||
let frequency_pass = frequency_error_hz <= t.max_frequency_error_hz;
|
||||
let av_sync_pass = m.av_sync_drift_ms.abs() <= t.max_av_sync_drift_ms;
|
||||
// Stop latency must be finite and within bound (a missing/NaN measurement
|
||||
// fails closed).
|
||||
let stop_latency_pass =
|
||||
m.stop_latency_ms.is_finite() && m.stop_latency_ms <= t.max_stop_latency_ms;
|
||||
let hash_reproducibility = if m.hashes_total > 0 {
|
||||
m.hashes_reproduced as f64 / m.hashes_total as f64
|
||||
} else {
|
||||
0.0 // nothing replayed ⇒ unproven ⇒ fail closed
|
||||
};
|
||||
let hash_pass = hash_reproducibility >= t.min_hash_reproducibility;
|
||||
let baseline = m.eeg_entrainment_fixed_40hz.max(1e-6);
|
||||
let eeg_lift = (m.eeg_entrainment_adaptive - baseline) / baseline;
|
||||
let eeg_lift_pass = eeg_lift >= t.min_eeg_lift;
|
||||
|
||||
let overall_pass =
|
||||
frequency_pass && av_sync_pass && stop_latency_pass && hash_pass && eeg_lift_pass;
|
||||
|
||||
HilReport {
|
||||
frequency_error_hz,
|
||||
frequency_pass,
|
||||
av_sync_pass,
|
||||
stop_latency_pass,
|
||||
hash_reproducibility,
|
||||
hash_pass,
|
||||
eeg_lift,
|
||||
eeg_lift_pass,
|
||||
overall_pass,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn passing() -> HilMeasurement {
|
||||
HilMeasurement {
|
||||
commanded_frequency_hz: 40.0,
|
||||
measured_frequency_hz: 40.05, // within ±0.1 Hz
|
||||
av_sync_drift_ms: 2.0, // < 5 ms
|
||||
stop_latency_ms: 40.0, // < 100 ms
|
||||
hashes_reproduced: 100,
|
||||
hashes_total: 100, // 100%
|
||||
eeg_entrainment_adaptive: 0.36,
|
||||
eeg_entrainment_fixed_40hz: 0.30, // +20%
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_good_bench_run_passes_all_criteria() {
|
||||
let r = verify_hil(&passing(), &HilTargets::default());
|
||||
assert!(r.overall_pass);
|
||||
assert!(r.frequency_error_hz <= 0.1);
|
||||
assert!((r.eeg_lift - 0.20).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frequency_drift_beyond_tenth_hz_fails() {
|
||||
let mut m = passing();
|
||||
m.measured_frequency_hz = 40.3; // 0.3 Hz error
|
||||
let r = verify_hil(&m, &HilTargets::default());
|
||||
assert!(!r.frequency_pass);
|
||||
assert!(!r.overall_pass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slow_stop_fails() {
|
||||
let mut m = passing();
|
||||
m.stop_latency_ms = 250.0;
|
||||
assert!(!verify_hil(&m, &HilTargets::default()).stop_latency_pass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_stop_measurement_fails_closed() {
|
||||
let mut m = passing();
|
||||
m.stop_latency_ms = f64::NAN;
|
||||
assert!(!verify_hil(&m, &HilTargets::default()).stop_latency_pass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_hash_mismatch_fails_reproducibility() {
|
||||
let mut m = passing();
|
||||
m.hashes_reproduced = 99; // one of 100 drifted
|
||||
let r = verify_hil(&m, &HilTargets::default());
|
||||
assert!(!r.hash_pass);
|
||||
assert!(!r.overall_pass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_replay_fails_closed() {
|
||||
let mut m = passing();
|
||||
m.hashes_reproduced = 0;
|
||||
m.hashes_total = 0;
|
||||
assert!(!verify_hil(&m, &HilTargets::default()).hash_pass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insufficient_eeg_lift_fails() {
|
||||
let mut m = passing();
|
||||
m.eeg_entrainment_adaptive = 0.32; // only +6.7%
|
||||
let r = verify_hil(&m, &HilTargets::default());
|
||||
assert!(!r.eeg_lift_pass);
|
||||
assert!(!r.overall_pass);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_drift_beyond_5ms_fails() {
|
||||
let mut m = passing();
|
||||
m.av_sync_drift_ms = 7.5;
|
||||
assert!(!verify_hil(&m, &HilTargets::default()).av_sync_pass);
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@
|
||||
|
||||
pub mod acceptance;
|
||||
pub mod bandit;
|
||||
pub mod hil;
|
||||
pub mod math;
|
||||
pub mod objective;
|
||||
pub mod optimizer;
|
||||
|
||||
Reference in New Issue
Block a user