mirror of
https://github.com/ruvnet/RuView
synced 2026-06-28 13:23:19 +00:00
feat(ruview-gamma): generalize to adaptive sensory neuromodulation platform
40 Hz becomes one prior in one program, not the product. The engine is a personal neural-rhythm optimization platform: RuView is the feedback signal, RuVector the personal response map, the device the actuator, RuFlo the governed/auditable loop (ADR-250 section 23). 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, 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 "measurable entrainment, safety, adherence, repeatability before any disease claim" executable. AcceptanceHarness grades a program over >=3 repeats; ClaimGate releases the program's claim ONLY when all four pass, else the research-only NO_CLAIM string. The marketing claim is unreadable except through the gate. Governor: enroll_program (per-program envelope/objective), program(), prior(), state_eligible(). The bare enroll() path is unchanged, so the pinned witness 13cb164c... is preserved. 88 crate tests + 1 doctest; workspace gate 2,889 passed / 0 failed. Benches: program grading ~425us; hot paths unchanged (recommend ~15us, calibration ~115us, kNN/500 ~15us). 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` 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).
|
||||
- **RuView beyond-SOTA research series** (`docs/research/ruview-beyond-sota/`, 6 docs) — research-swarm output defining the beyond-SOTA bar and the path to it: system capability audit (role→crate maturity matrix, gap analysis, risk register), web-verified 2026 SOTA landscape per capability axis (incl. ratified IEEE 802.11bf-2025), 8-pillar target architecture on the ADR-136 contract spine (no rewrite), 6-layer benchmark/validation methodology (all 15 criterion bench targets inventoried; ADR-149 statistical protocol), and a determinism-safe optimization roadmap. Includes session validation evidence: 2,797 workspace tests / 0 failed, Python proof PASS (bit-exact), paired pre/post criterion runs.
|
||||
|
||||
@@ -22,7 +22,7 @@ Dual codebase: Python v1 (`v1/`) and Rust port (`v2/`).
|
||||
| `nvsim` | Deterministic NV-diamond magnetometer pipeline simulator (ADR-089) — standalone leaf, WASM-ready |
|
||||
| `vendor/rvcsi` (submodule) | **rvCSI** — edge RF sensing runtime (ADR-095/096): 9 crates (`rvcsi-core`/`-dsp`/`-events`/`-adapter-file`/`-adapter-nexmon`/`-ruvector`/`-runtime`/`-node`/`-cli`). Lives in its own repo ([github.com/ruvnet/rvcsi](https://github.com/ruvnet/rvcsi)), vendored here under `vendor/rvcsi`, published to crates.io as `rvcsi-* 0.3.x` and to npm as `@ruv/rvcsi`. Not a `v2/` workspace member — depend on the published crates (or the submodule's `crates/rvcsi-*` paths). Normalized `CsiFrame`/`CsiWindow`/`CsiEvent` schema, validate-before-FFI, reusable DSP, typed confidence-scored events, the napi-c Nexmon shim (real nexmon_csi `.pcap` from a Raspberry Pi 5 / 4 / 3B+ — BCM43455c0), the napi-rs SDK, the `rvcsi` CLI, a Claude Code plugin. |
|
||||
| `ruview-swarm` | Drone swarm control system (ADR-148) — hierarchical-mesh topology, Raft consensus, MARL, CSI sensing payload, MAVLink/PX4 compat, Ruflo AI-agent integration |
|
||||
| `ruview-gamma` | Adaptive Gamma Entrainment (ADR-250) — governed, deterministic, safety-constrained personalization of 40 Hz-prior light+sound stimulation (RuView sensing + RuVector response modeling + RuFlo audit). Standalone leaf, ChaCha20 + SHA-256 witness. Research platform, not a medical device. |
|
||||
| `ruview-gamma` | Adaptive Sensory Neuromodulation platform (ADR-250) — governed, deterministic, safety-constrained personal neural-rhythm optimization. 7-program `NeuroProgram` catalog (Alzheimer's research, post-stroke cognition, sleep, attention/WM, mood/arousal, home wellness, trial infrastructure), each with its own safety envelope/prior/objective/state-gating/evidence-level/gated-claim. RuView sensing + RuVector cross-person self-learning (cohort warm-start, drift, clustering) + RuFlo audit. Executable acceptance gate (entrainment/safety/adherence/repeatability) releases a claim only when passed. Standalone leaf, ChaCha20 + SHA-256 witness. Research platform, not a medical device. |
|
||||
|
||||
### RuvSense Modules (`signal/src/ruvsense/`)
|
||||
| Module | Purpose |
|
||||
|
||||
@@ -197,6 +197,47 @@ bit-exactly before any hardware or human exposure. Hardware actuation, real RF
|
||||
sensing, and real EEG land behind feature flags / external adapters; this crate
|
||||
implements the governed software core and its proofs.
|
||||
|
||||
## 23. Platform Generalization — Adaptive Sensory Neuromodulation
|
||||
|
||||
The broader opportunity is **adaptive sensory neuromodulation**, not just
|
||||
Alzheimer's. 40 Hz is one prior in one program; the engine is a personal
|
||||
neural-rhythm optimization platform. RuView turns the body into the feedback
|
||||
signal, RuVector turns repeated sessions into a personal response map, the
|
||||
device is the actuator, and RuFlo makes the loop governed and auditable.
|
||||
|
||||
Each use case is a `NeuroProgram` (`program.rs`) bundling its own safety
|
||||
envelope, starting prior, objective weighting, physiological-state gating,
|
||||
evidence level, and the single non-disease claim it may surface:
|
||||
|
||||
| Program | Evidence level | RuView / RuVector role | Released claim |
|
||||
|---------|----------------|------------------------|----------------|
|
||||
| `alzheimers-research` | Medium preclinical, early human | Adaptive entrainment + trial monitoring | personalized entrainment optimization |
|
||||
| `post-stroke-cognition` | Early human | Recovery-state tracking (ramped, comfort-weighted) | …with recovery-state monitoring |
|
||||
| `sleep-optimization` | Early but plausible | Time stimulation to sleep state (audio, near-dark cap) | sleep-state-timed entrainment optimization |
|
||||
| `attention-working-memory` | Mixed / protocol-dependent | Personal frequency discovery (entrainment-weighted) | personalized frequency-response discovery |
|
||||
| `mood-arousal` | Early human | Avoid overstimulation, tune calming response | personalized calming-response optimization |
|
||||
| `home-wellness` | Speculative | Safe personalization without treatment claims | personal neural-rhythm wellness optimization |
|
||||
| `trial-infrastructure` | Strong infrastructure | Governed protocol/safety/consent/sham log | governed, reproducible protocol measurement |
|
||||
|
||||
**Claim discipline is structural.** A program's claim is always an
|
||||
optimization/monitoring statement, never a disease-treatment claim; the disease
|
||||
*context* lives only in `EvidenceLevel`. A claim is releasable **only** through
|
||||
the acceptance gate.
|
||||
|
||||
### 23.1 Generalized acceptance gate (`acceptance.rs`)
|
||||
|
||||
> Every use case must show measurable **entrainment**, **safety**,
|
||||
> **adherence**, and **repeatability** before making any disease claim.
|
||||
|
||||
`AcceptanceHarness::evaluate(program, person, state)` runs the program over ≥3
|
||||
independent repeats and measures: adaptive-vs-fixed-prior entrainment gain,
|
||||
safety-stop rate, mean adherence, and the spread of the discovered optimal
|
||||
frequency. The resulting `AcceptanceReport` exposes a `ClaimGate` that returns
|
||||
the program's claim **iff all four criteria pass**, and the research-only
|
||||
`NO_CLAIM` string otherwise — the program's marketing claim cannot be read
|
||||
except through this gate. This makes the acceptance sentence executable, not
|
||||
aspirational, and applies uniformly to all seven programs.
|
||||
|
||||
## 22. Final Decision Statement
|
||||
|
||||
We build Adaptive Gamma Entrainment as a governed RuView + RuVector
|
||||
|
||||
@@ -43,6 +43,8 @@ conservative floor, never the cap.
|
||||
| `optimizer` | §8 | Phase-1 calibration sweep, Phase-2 GP + Expected-Improvement, Phase-4 closed-loop control |
|
||||
| `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 |
|
||||
| `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`) |
|
||||
@@ -94,6 +96,39 @@ the optimizer, simulator, response update, or session hashing.
|
||||
| `gamma_calibration_sweep` | ~135 µs | full 9-session enroll → simulate → score → update → witness (was ~486 µs, −71%) |
|
||||
| `gamma_cohort_knn_500` | ~15 µs | exact kNN over 500 anonymized profiles |
|
||||
| `gamma_cohort_warm_start_500` | ~16 µs | full cohort prior construction (runs once per enrollment) |
|
||||
| `gamma_acceptance_grade_program` | ~425 µs | full 3-repeat program acceptance grading (offline gate) |
|
||||
|
||||
## Adaptive sensory neuromodulation platform (ADR-250 §23)
|
||||
|
||||
40 Hz is one prior in one program — the engine is a general personal
|
||||
neural-rhythm optimization platform. `NeuroProgram::catalog()` ships seven 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** safety envelope, prior, objective
|
||||
weighting, physiological-state gating (the sleep program permits `Asleep` and
|
||||
caps brightness near-dark; attention requires wakefulness), evidence level, and
|
||||
a single non-disease claim. `RufloGovernor::enroll_program` wires it all in;
|
||||
`enroll` stays the bare Alzheimer's-defaults path (so the pinned witness holds).
|
||||
|
||||
**Claim discipline is executable.** A program's claim can only be read through
|
||||
the acceptance gate:
|
||||
|
||||
```rust
|
||||
use ruview_gamma::acceptance::{AcceptanceHarness, AcceptanceCriteria};
|
||||
use ruview_gamma::program::NeuroProgram;
|
||||
# use ruview_gamma::simulator::LatentPerson;
|
||||
# use ruview_gamma::response::RuViewState;
|
||||
|
||||
let harness = AcceptanceHarness::new(42, AcceptanceCriteria::default());
|
||||
let report = harness.evaluate(
|
||||
&NeuroProgram::sleep_optimization(),
|
||||
&LatentPerson::from_id("subject"),
|
||||
&RuViewState::calm_baseline(),
|
||||
);
|
||||
// Returns the program claim ONLY if entrainment + safety + adherence +
|
||||
// repeatability all pass; otherwise the research-only NO_CLAIM string.
|
||||
let _claim = report.claim_gate().claim();
|
||||
```
|
||||
|
||||
## Self-learning across people (ADR-250 §10)
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use ruview_gamma::acceptance::{AcceptanceCriteria, AcceptanceHarness};
|
||||
use ruview_gamma::bandit::{BanditContext, ContextualBandit};
|
||||
use ruview_gamma::optimizer::BayesianOptimizer;
|
||||
use ruview_gamma::program::NeuroProgram;
|
||||
use ruview_gamma::response::RuViewState;
|
||||
use ruview_gamma::ruflo::{Consent, RufloGovernor};
|
||||
use ruview_gamma::ruvector::{AnonymizedProfile, ProfileStore, VECTOR_DIM};
|
||||
@@ -109,12 +111,23 @@ fn bench_cohort_knn(c: &mut Criterion) {
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_acceptance(c: &mut Criterion) {
|
||||
let harness = AcceptanceHarness::new(42, AcceptanceCriteria::default());
|
||||
let program = NeuroProgram::sleep_optimization();
|
||||
let person = LatentPerson::from_id("bench-acc-subject");
|
||||
let state = RuViewState::calm_baseline();
|
||||
c.bench_function("gamma_acceptance_grade_program", |b| {
|
||||
b.iter(|| black_box(harness.evaluate(black_box(&program), &person, &state)))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_calibration,
|
||||
bench_recommend,
|
||||
bench_safety_tick,
|
||||
bench_bandit,
|
||||
bench_cohort_knn
|
||||
bench_cohort_knn,
|
||||
bench_acceptance
|
||||
);
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
//! Acceptance gate (ADR-250 §18, generalized across programs).
|
||||
//!
|
||||
//! > Every use case must show measurable **entrainment**, **safety**,
|
||||
//! > **adherence**, and **repeatability** before making any disease claim.
|
||||
//!
|
||||
//! This module operationalizes that sentence. [`AcceptanceHarness`] runs a
|
||||
//! [`NeuroProgram`] against the deterministic simulator over several
|
||||
//! independent repeats and produces an [`AcceptanceReport`] with the four
|
||||
//! measured metrics, a per-criterion pass/fail, and — crucially — a
|
||||
//! [`ClaimGate`] that **only releases the program's claim when every criterion
|
||||
//! passes**. Until then the gate returns the research-only, no-claim string.
|
||||
|
||||
use crate::program::NeuroProgram;
|
||||
use crate::response::RuViewState;
|
||||
use crate::ruflo::{Consent, RufloGovernor};
|
||||
use crate::simulator::{LatentPerson, ResponseSimulator};
|
||||
use crate::stimulus::StimulusParameters;
|
||||
|
||||
/// The research-only string returned by a failed [`ClaimGate`]: no optimization
|
||||
/// 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";
|
||||
|
||||
/// Thresholds a program must clear (ADR-250 §18 generalized). Defaults mirror
|
||||
/// the ADR's published targets; programs may tighten them.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct AcceptanceCriteria {
|
||||
/// Minimum mean entrainment gain of the adaptive recommendation over the
|
||||
/// program's fixed prior, as a fraction (0.20 = ADR-250 §18 "≥20%").
|
||||
pub min_entrainment_gain: f64,
|
||||
/// Maximum tolerated safety-stop rate across all sessions (0.0 = none).
|
||||
pub max_safety_stop_rate: f64,
|
||||
/// Minimum mean adherence across sessions.
|
||||
pub min_adherence: f64,
|
||||
/// Maximum spread (Hz) of the discovered optimal frequency across repeats
|
||||
/// (ADR-250 §18 "same optimal band within ±1 Hz across 3 sessions" → 2 Hz).
|
||||
pub max_repeatability_band_hz: f64,
|
||||
/// Independent repeats to run (≥3 per ADR-250 §18).
|
||||
pub repeats: usize,
|
||||
}
|
||||
|
||||
impl Default for AcceptanceCriteria {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_entrainment_gain: 0.20,
|
||||
max_safety_stop_rate: 0.0,
|
||||
min_adherence: 0.8,
|
||||
max_repeatability_band_hz: 2.0,
|
||||
repeats: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The four measured metrics plus per-criterion verdicts and the gated claim.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AcceptanceReport {
|
||||
pub program_id: String,
|
||||
/// Mean entrainment gain (adaptive vs fixed prior), as a fraction.
|
||||
pub entrainment_gain: f64,
|
||||
/// Observed safety-stop rate across all sessions.
|
||||
pub safety_stop_rate: f64,
|
||||
/// Mean adherence across all sessions.
|
||||
pub mean_adherence: f64,
|
||||
/// Spread (Hz) of the discovered optimal frequency across repeats.
|
||||
pub repeatability_band_hz: f64,
|
||||
pub entrainment_pass: bool,
|
||||
pub safety_pass: bool,
|
||||
pub adherence_pass: bool,
|
||||
pub repeatability_pass: bool,
|
||||
/// True only if all four criteria pass.
|
||||
pub overall_pass: bool,
|
||||
/// The claim that may be surfaced: the program's claim iff `overall_pass`,
|
||||
/// else [`NO_CLAIM`].
|
||||
pub released_claim: String,
|
||||
}
|
||||
|
||||
impl AcceptanceReport {
|
||||
/// The [`ClaimGate`] for this report.
|
||||
pub fn claim_gate(&self) -> ClaimGate<'_> {
|
||||
ClaimGate { report: self }
|
||||
}
|
||||
}
|
||||
|
||||
/// A thin, hard-to-misuse accessor: you cannot read a program's marketing claim
|
||||
/// except through this gate, which substitutes [`NO_CLAIM`] on failure.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ClaimGate<'a> {
|
||||
report: &'a AcceptanceReport,
|
||||
}
|
||||
|
||||
impl ClaimGate<'_> {
|
||||
/// The releasable claim string (program claim on pass, [`NO_CLAIM`] on fail).
|
||||
pub fn claim(&self) -> &str {
|
||||
&self.report.released_claim
|
||||
}
|
||||
|
||||
/// Whether a (non-disease) optimization claim may be surfaced at all.
|
||||
pub fn is_released(&self) -> bool {
|
||||
self.report.overall_pass
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a program against the deterministic simulator and grades it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AcceptanceHarness {
|
||||
pub criteria: AcceptanceCriteria,
|
||||
seed: u64,
|
||||
}
|
||||
|
||||
impl AcceptanceHarness {
|
||||
pub fn new(seed: u64, criteria: AcceptanceCriteria) -> Self {
|
||||
Self { criteria, seed }
|
||||
}
|
||||
|
||||
/// Grade `program` for a simulated participant `person` in `state`.
|
||||
///
|
||||
/// Each repeat: enroll under the program, run its calibration sweep, take
|
||||
/// the adaptive recommendation, and compare its mean simulated entrainment
|
||||
/// against the program's fixed prior. Metrics are aggregated across repeats;
|
||||
/// the claim is released only if all four criteria pass.
|
||||
pub fn evaluate(
|
||||
&self,
|
||||
program: &NeuroProgram,
|
||||
person: &LatentPerson,
|
||||
state: &RuViewState,
|
||||
) -> AcceptanceReport {
|
||||
let sim = ResponseSimulator::new(self.seed);
|
||||
let mut optimal_freqs = Vec::with_capacity(self.criteria.repeats);
|
||||
let mut gains = Vec::with_capacity(self.criteria.repeats);
|
||||
let mut total_sessions = 0usize;
|
||||
let mut total_stops = 0usize;
|
||||
let mut adherence_sum = 0.0;
|
||||
|
||||
for r in 0..self.criteria.repeats.max(1) {
|
||||
let pid = format!("acc-{}-{}", program.id, r);
|
||||
let mut gov = match RufloGovernor::enroll_program(&pid, program.clone(), &[], Consent::Granted)
|
||||
{
|
||||
Ok(g) => g,
|
||||
// A program that cannot enroll a clean participant fails closed.
|
||||
Err(_) => return self.failed_report(program, "enrollment_failed"),
|
||||
};
|
||||
// Vary the noise stream per repeat so repeatability is a real test.
|
||||
gov.run_calibration(&sim, person, state, program.prior.duration_minutes.min(5.0), r as u64)
|
||||
.ok();
|
||||
|
||||
for rec in gov.audit_log() {
|
||||
total_sessions += 1;
|
||||
if !rec.outcome.safety_pass {
|
||||
total_stops += 1;
|
||||
}
|
||||
adherence_sum += rec.ruview_state.adherence as f64;
|
||||
}
|
||||
|
||||
let rec = gov.recommend(&program.prior);
|
||||
optimal_freqs.push(rec.stimulus.frequency_hz);
|
||||
|
||||
// Entrainment gain: adaptive recommendation vs the fixed prior.
|
||||
let mean = |stim: &StimulusParameters| -> f64 {
|
||||
(0..16)
|
||||
.map(|i| sim.simulate(person, state, stim, 10_000 + i).eeg.gamma_power_gain)
|
||||
.sum::<f64>()
|
||||
/ 16.0
|
||||
};
|
||||
let adaptive = mean(&rec.stimulus);
|
||||
let baseline = mean(&program.prior).max(1e-6);
|
||||
gains.push((adaptive - baseline) / baseline);
|
||||
}
|
||||
|
||||
let entrainment_gain = mean_of(&gains);
|
||||
let safety_stop_rate = if total_sessions > 0 {
|
||||
total_stops as f64 / total_sessions as f64
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let mean_adherence = if total_sessions > 0 {
|
||||
adherence_sum / total_sessions as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let repeatability_band_hz = spread(&optimal_freqs);
|
||||
|
||||
let c = &self.criteria;
|
||||
let entrainment_pass = entrainment_gain >= c.min_entrainment_gain;
|
||||
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;
|
||||
|
||||
AcceptanceReport {
|
||||
program_id: program.id.to_string(),
|
||||
entrainment_gain,
|
||||
safety_stop_rate,
|
||||
mean_adherence,
|
||||
repeatability_band_hz,
|
||||
entrainment_pass,
|
||||
safety_pass,
|
||||
adherence_pass,
|
||||
repeatability_pass,
|
||||
overall_pass,
|
||||
released_claim: if overall_pass {
|
||||
program.claim.to_string()
|
||||
} else {
|
||||
NO_CLAIM.to_string()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn failed_report(&self, program: &NeuroProgram, _why: &str) -> AcceptanceReport {
|
||||
AcceptanceReport {
|
||||
program_id: program.id.to_string(),
|
||||
entrainment_gain: 0.0,
|
||||
safety_stop_rate: 1.0,
|
||||
mean_adherence: 0.0,
|
||||
repeatability_band_hz: f64::INFINITY,
|
||||
entrainment_pass: false,
|
||||
safety_pass: false,
|
||||
adherence_pass: false,
|
||||
repeatability_pass: false,
|
||||
overall_pass: false,
|
||||
released_claim: NO_CLAIM.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mean_of(v: &[f64]) -> f64 {
|
||||
if v.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
v.iter().sum::<f64>() / v.len() as f64
|
||||
}
|
||||
}
|
||||
|
||||
fn spread(v: &[f64]) -> f64 {
|
||||
if v.is_empty() {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
let lo = v.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
let hi = v.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
hi - lo
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::response::{RuViewState, SleepState};
|
||||
|
||||
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.
|
||||
for n in 0..80 {
|
||||
let id = format!("acc-subject-{n}");
|
||||
let p = LatentPerson::from_id(&id);
|
||||
if (p.peak_hz - 40.0).abs() > 2.0 && p.peak_hz > 37.5 && p.peak_hz < 42.5 {
|
||||
return (id, p);
|
||||
}
|
||||
}
|
||||
panic!("a detuned subject exists");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_is_withheld_until_criteria_pass() {
|
||||
// Impossible entrainment bar → must fail → NO_CLAIM.
|
||||
let (_, person) = detuned_subject();
|
||||
let harness = AcceptanceHarness::new(
|
||||
1,
|
||||
AcceptanceCriteria {
|
||||
min_entrainment_gain: 100.0, // unreachable
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let report =
|
||||
harness.evaluate(&NeuroProgram::attention_working_memory(), &person, &RuViewState::calm_baseline());
|
||||
assert!(!report.overall_pass);
|
||||
assert!(!report.entrainment_pass);
|
||||
assert_eq!(report.claim_gate().claim(), NO_CLAIM);
|
||||
assert!(!report.claim_gate().is_released());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passing_program_releases_its_own_claim() {
|
||||
let (_, person) = detuned_subject();
|
||||
let program = NeuroProgram::attention_working_memory();
|
||||
// Lenient-but-real bar: any positive adaptive gain, perfect sim safety/adherence.
|
||||
let harness = AcceptanceHarness::new(
|
||||
7,
|
||||
AcceptanceCriteria {
|
||||
min_entrainment_gain: 0.0,
|
||||
max_safety_stop_rate: 0.0,
|
||||
min_adherence: 0.8,
|
||||
max_repeatability_band_hz: 8.0,
|
||||
repeats: 3,
|
||||
},
|
||||
);
|
||||
let report = harness.evaluate(&program, &person, &RuViewState::calm_baseline());
|
||||
assert!(report.entrainment_pass);
|
||||
assert!(report.safety_pass);
|
||||
assert!(report.adherence_pass);
|
||||
if report.overall_pass {
|
||||
assert_eq!(report.claim_gate().claim(), program.claim);
|
||||
assert!(report.claim_gate().is_released());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safety_criterion_blocks_claim_on_stops() {
|
||||
// Drive overstimulation so the simulator raises adverse events: a
|
||||
// saturated-intensity prior in a restless state.
|
||||
let (_, person) = detuned_subject();
|
||||
let harness = AcceptanceHarness::new(3, AcceptanceCriteria::default());
|
||||
let mut restless = RuViewState::calm_baseline();
|
||||
restless.sleep_state = SleepState::Active;
|
||||
restless.restlessness_score = 0.9;
|
||||
// Even if it passes, the safety rate must be a real measured fraction.
|
||||
let report = harness.evaluate(&NeuroProgram::mood_arousal(), &person, &restless);
|
||||
assert!((0.0..=1.0).contains(&report.safety_stop_rate));
|
||||
if report.safety_stop_rate > 0.0 {
|
||||
assert!(!report.safety_pass);
|
||||
assert!(!report.overall_pass);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_catalog_program_is_gradable() {
|
||||
let (_, person) = detuned_subject();
|
||||
let harness = AcceptanceHarness::new(11, AcceptanceCriteria::default());
|
||||
let state = RuViewState::calm_baseline();
|
||||
for program in NeuroProgram::catalog() {
|
||||
let report = harness.evaluate(&program, &person, &state);
|
||||
assert_eq!(report.program_id, program.id);
|
||||
// Gate is total: it always yields *some* releasable string.
|
||||
assert!(!report.released_claim.is_empty());
|
||||
// And a failing program never leaks the program claim.
|
||||
if !report.overall_pass {
|
||||
assert_eq!(report.claim_gate().claim(), NO_CLAIM);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,10 +68,12 @@
|
||||
//! assert!(envelope.contains(&rec.stimulus)); // always inside the envelope
|
||||
//! ```
|
||||
|
||||
pub mod acceptance;
|
||||
pub mod bandit;
|
||||
pub mod math;
|
||||
pub mod objective;
|
||||
pub mod optimizer;
|
||||
pub mod program;
|
||||
pub mod proof;
|
||||
pub mod response;
|
||||
pub mod ruflo;
|
||||
@@ -165,6 +167,78 @@ mod integration_tests {
|
||||
assert_eq!(warm.clinician_report().n_sessions, 0);
|
||||
}
|
||||
|
||||
/// Platform extension: each program enforces its **own** safety envelope.
|
||||
/// A stimulus that is fine for the Alzheimer's program (brightness 0.30)
|
||||
/// exceeds the sleep program's near-dark cap (0.10) and is refused.
|
||||
#[test]
|
||||
fn per_program_envelope_is_enforced() {
|
||||
use crate::program::NeuroProgram;
|
||||
use crate::stimulus::StimulusParameters;
|
||||
|
||||
let sim = ResponseSimulator::new(5);
|
||||
let latent = LatentPerson::from_id("prog-subject");
|
||||
let state = RuViewState::calm_baseline();
|
||||
|
||||
// 0.30 brightness: inside Alzheimer's envelope, outside sleep's (0.10).
|
||||
let mut stim = StimulusParameters::prior();
|
||||
stim.frequency_hz = 40.0;
|
||||
stim.brightness_level = 0.30;
|
||||
|
||||
let mut alz = RufloGovernor::enroll_program(
|
||||
"p1",
|
||||
NeuroProgram::alzheimers_research(),
|
||||
&[],
|
||||
Consent::Granted,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(alz.run_session(&sim, &latent, &state, &stim, 0).is_ok());
|
||||
|
||||
let mut sleep = RufloGovernor::enroll_program(
|
||||
"p2",
|
||||
NeuroProgram::sleep_optimization(),
|
||||
&[],
|
||||
Consent::Granted,
|
||||
)
|
||||
.unwrap();
|
||||
// The same stimulus is out-of-envelope for the sleep program.
|
||||
assert!(sleep.run_session(&sim, &latent, &state, &stim, 0).is_err());
|
||||
// Its own prior (audio, near-dark) is accepted.
|
||||
let prior = sleep.prior();
|
||||
assert!(sleep.program().unwrap().envelope.contains(&prior));
|
||||
}
|
||||
|
||||
/// Platform acceptance matrix: every catalog program is gradable and the
|
||||
/// claim gate is total (always yields a releasable string; a failing
|
||||
/// program never leaks its marketing claim).
|
||||
#[test]
|
||||
fn acceptance_matrix_gates_every_program_claim() {
|
||||
use crate::acceptance::{AcceptanceCriteria, AcceptanceHarness, NO_CLAIM};
|
||||
use crate::program::NeuroProgram;
|
||||
|
||||
// A detuned subject so adaptive programs have real gain to find.
|
||||
let mut chosen = None;
|
||||
for n in 0..80 {
|
||||
let id = format!("matrix-{n}");
|
||||
let p = LatentPerson::from_id(&id);
|
||||
if (p.peak_hz - 40.0).abs() > 2.0 && p.peak_hz > 37.5 && p.peak_hz < 42.5 {
|
||||
chosen = Some(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let person = chosen.expect("detuned subject");
|
||||
let harness = AcceptanceHarness::new(42, AcceptanceCriteria::default());
|
||||
let state = RuViewState::calm_baseline();
|
||||
for program in NeuroProgram::catalog() {
|
||||
let report = harness.evaluate(&program, &person, &state);
|
||||
assert!(!report.released_claim.is_empty());
|
||||
if report.overall_pass {
|
||||
assert_eq!(report.claim_gate().claim(), program.claim);
|
||||
} else {
|
||||
assert_eq!(report.claim_gate().claim(), NO_CLAIM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ADR-250 §10 item 4: a stable participant stays `Stable`; collapsing
|
||||
/// their physiology (restless, uncomfortable, no entrainment) flags
|
||||
/// `Drifted`, recommending recalibration.
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
//! Neuromodulation programs (ADR-250 extension: adaptive sensory
|
||||
//! neuromodulation, not just Alzheimer's).
|
||||
//!
|
||||
//! The platform thesis: RuView turns the body into the feedback signal,
|
||||
//! RuVector turns repeated sessions into a personal response map, the device is
|
||||
//! the actuator, and RuFlo makes the loop governed and auditable. The *real*
|
||||
//! product is a personal neural-rhythm optimization platform — and each use
|
||||
//! case is a [`NeuroProgram`] bundling its own safety envelope, starting prior,
|
||||
//! objective weighting, physiological-state gating, evidence level, and the
|
||||
//! single claim it is allowed to make.
|
||||
//!
|
||||
//! **Claim discipline is structural:** a program's [`NeuroProgram::claim`] is
|
||||
//! always an *optimization / monitoring* statement, never a disease-treatment
|
||||
//! claim. The disease context lives only in [`EvidenceLevel`], and a claim is
|
||||
//! only releasable once the program clears its acceptance gate
|
||||
//! (`crate::acceptance`).
|
||||
|
||||
use crate::objective::ObjectiveWeights;
|
||||
use crate::response::SleepState;
|
||||
use crate::stimulus::{DutyCycle, Modality, SafetyEnvelope, StimulusParameters};
|
||||
|
||||
/// How well-supported a program's *disease/context* hypothesis is in the
|
||||
/// literature (the user's opportunity map). This gates nothing by itself — it
|
||||
/// is metadata a clinician/operator reads alongside the acceptance report.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EvidenceLevel {
|
||||
/// Preclinical + early human (e.g. Alzheimer's gamma work).
|
||||
MediumPreclinicalEarlyHuman,
|
||||
/// Early human signals only (post-stroke, sleep, mood).
|
||||
EarlyHuman,
|
||||
/// Mixed / protocol-dependent (attention, working memory).
|
||||
Mixed,
|
||||
/// Speculative (home neuro-wellness).
|
||||
Speculative,
|
||||
/// Strong *infrastructure* opportunity (drug+device trial monitoring) —
|
||||
/// the strength is in measurement/governance, not a therapeutic claim.
|
||||
StrongInfrastructure,
|
||||
}
|
||||
|
||||
impl EvidenceLevel {
|
||||
pub fn tag(self) -> &'static str {
|
||||
match self {
|
||||
EvidenceLevel::MediumPreclinicalEarlyHuman => "medium_preclinical_early_human",
|
||||
EvidenceLevel::EarlyHuman => "early_human",
|
||||
EvidenceLevel::Mixed => "mixed_protocol_dependent",
|
||||
EvidenceLevel::Speculative => "speculative",
|
||||
EvidenceLevel::StrongInfrastructure => "strong_infrastructure",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Time-of-day preference for a program (state-dependent entrainment).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TimePreference {
|
||||
Morning,
|
||||
Evening,
|
||||
/// Quiet wakefulness (the Alzheimer's/attention default).
|
||||
QuietWake,
|
||||
/// Pre-sleep / during sleep (the sleep program).
|
||||
PreSleepOrSleep,
|
||||
/// Any time.
|
||||
Any,
|
||||
}
|
||||
|
||||
/// A named neuromodulation program: everything that distinguishes one use case
|
||||
/// from another, in one value.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NeuroProgram {
|
||||
/// Stable machine id (used in the session witness / provenance).
|
||||
pub id: &'static str,
|
||||
/// Human-readable name.
|
||||
pub display_name: &'static str,
|
||||
/// Literature support for the *context* hypothesis.
|
||||
pub evidence_level: EvidenceLevel,
|
||||
/// The ONLY claim this program may surface (an optimization/monitoring
|
||||
/// statement — never a disease-treatment claim).
|
||||
pub claim: &'static str,
|
||||
/// Per-program safety envelope (optimization happens only inside it).
|
||||
pub envelope: SafetyEnvelope,
|
||||
/// Evidence-based starting prior for this program.
|
||||
pub prior: StimulusParameters,
|
||||
/// Objective weighting tuned to the program's goal.
|
||||
pub weights: ObjectiveWeights,
|
||||
/// Physiological states in which a session is *protocol-eligible* (e.g. the
|
||||
/// sleep program permits `Asleep`; attention requires wakefulness).
|
||||
pub eligible_states: &'static [SleepState],
|
||||
/// When the program prefers to run.
|
||||
pub time_preference: TimePreference,
|
||||
}
|
||||
|
||||
impl NeuroProgram {
|
||||
/// Whether a session in `state` fits this program's protocol. This is a
|
||||
/// *protocol-fit* gate (measured by acceptance), distinct from the hard
|
||||
/// *safety* gate (`SafetyEnvelope` / `SafetyMonitor`).
|
||||
pub fn state_eligible(&self, state: SleepState) -> bool {
|
||||
self.eligible_states.contains(&state)
|
||||
}
|
||||
|
||||
// ---- The opportunity map (one constructor per use case) ----
|
||||
|
||||
/// Alzheimer's research — adaptive entrainment + trial monitoring. Matches
|
||||
/// the original ADR-250 defaults (conservative envelope, 40 Hz prior,
|
||||
/// default weights), so the `RufloGovernor::enroll` witness is unchanged.
|
||||
pub fn alzheimers_research() -> Self {
|
||||
Self {
|
||||
id: "alzheimers-research",
|
||||
display_name: "Alzheimer's Research (adaptive entrainment + trial monitoring)",
|
||||
evidence_level: EvidenceLevel::MediumPreclinicalEarlyHuman,
|
||||
claim: "personalized entrainment optimization",
|
||||
envelope: SafetyEnvelope::conservative(),
|
||||
prior: StimulusParameters::prior(),
|
||||
weights: ObjectiveWeights::default(),
|
||||
eligible_states: &[SleepState::QuietWake, SleepState::Drowsy],
|
||||
time_preference: TimePreference::QuietWake,
|
||||
}
|
||||
}
|
||||
|
||||
/// Post-stroke cognition — recovery state tracking. Comfort-leaning, gentle
|
||||
/// onset (ramped), short sessions; recovery populations tolerate less.
|
||||
pub fn post_stroke_cognition() -> Self {
|
||||
let mut prior = StimulusParameters::prior();
|
||||
prior.duty_cycle = DutyCycle::Ramped;
|
||||
prior.brightness_level = 0.25;
|
||||
prior.volume_level = 0.24;
|
||||
prior.duration_minutes = 8.0;
|
||||
let mut weights = ObjectiveWeights::default();
|
||||
weights.comfort = 0.20;
|
||||
weights.breathing_stability = 0.15;
|
||||
weights.gamma_gain = 0.25;
|
||||
weights.overstimulation = 0.15;
|
||||
Self {
|
||||
id: "post-stroke-cognition",
|
||||
display_name: "Post-Stroke Cognition (recovery state tracking)",
|
||||
evidence_level: EvidenceLevel::EarlyHuman,
|
||||
claim: "personalized entrainment optimization with recovery-state monitoring",
|
||||
envelope: SafetyEnvelope {
|
||||
brightness_cap: 0.32,
|
||||
volume_cap: 0.32,
|
||||
max_duration_minutes: 12.0,
|
||||
..SafetyEnvelope::conservative()
|
||||
},
|
||||
prior,
|
||||
weights,
|
||||
eligible_states: &[SleepState::QuietWake, SleepState::Drowsy],
|
||||
time_preference: TimePreference::Morning,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sleep optimization — time stimulation to sleep state. Permits `Drowsy`/
|
||||
/// `Asleep`, lowest intensity caps (must not degrade sleep), weights calm
|
||||
/// physiology over raw gamma.
|
||||
pub fn sleep_optimization() -> Self {
|
||||
let mut prior = StimulusParameters::prior();
|
||||
prior.modality = Modality::Audio; // light flicker is disruptive at sleep
|
||||
prior.brightness_level = 0.0;
|
||||
prior.volume_level = 0.18;
|
||||
prior.duty_cycle = DutyCycle::Ramped;
|
||||
prior.duration_minutes = 15.0;
|
||||
let mut weights = ObjectiveWeights::default();
|
||||
weights.gamma_gain = 0.20;
|
||||
weights.phase_locking = 0.20;
|
||||
weights.breathing_stability = 0.25; // calm sleep physiology is the point
|
||||
weights.comfort = 0.15;
|
||||
weights.overstimulation = 0.20;
|
||||
Self {
|
||||
id: "sleep-optimization",
|
||||
display_name: "Sleep Optimization (state-timed gamma)",
|
||||
evidence_level: EvidenceLevel::EarlyHuman,
|
||||
claim: "sleep-state-timed entrainment optimization",
|
||||
envelope: SafetyEnvelope {
|
||||
brightness_cap: 0.10, // near-dark
|
||||
volume_cap: 0.25,
|
||||
max_duration_minutes: 30.0,
|
||||
..SafetyEnvelope::conservative()
|
||||
},
|
||||
prior,
|
||||
weights,
|
||||
eligible_states: &[SleepState::Drowsy, SleepState::Asleep, SleepState::QuietWake],
|
||||
time_preference: TimePreference::PreSleepOrSleep,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attention & working memory — personal frequency discovery. Evidence is
|
||||
/// mixed/protocol-dependent, so this program leans hardest on *entrainment*
|
||||
/// terms (find the individual's responsive frequency) under wakefulness.
|
||||
pub fn attention_working_memory() -> Self {
|
||||
let mut weights = ObjectiveWeights::default();
|
||||
weights.gamma_gain = 0.35;
|
||||
weights.phase_locking = 0.30;
|
||||
weights.comfort = 0.10;
|
||||
Self {
|
||||
id: "attention-working-memory",
|
||||
display_name: "Attention & Working Memory (personal frequency discovery)",
|
||||
evidence_level: EvidenceLevel::Mixed,
|
||||
claim: "personalized frequency-response discovery",
|
||||
envelope: SafetyEnvelope::conservative(),
|
||||
prior: StimulusParameters::prior(),
|
||||
weights,
|
||||
eligible_states: &[SleepState::QuietWake, SleepState::Active],
|
||||
time_preference: TimePreference::QuietWake,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mood & arousal regulation — avoid overstimulation, tune the calming
|
||||
/// response. Lowest gamma weight, highest comfort + overstimulation penalty.
|
||||
pub fn mood_arousal() -> Self {
|
||||
let mut prior = StimulusParameters::prior();
|
||||
prior.brightness_level = 0.22;
|
||||
prior.volume_level = 0.22;
|
||||
prior.duty_cycle = DutyCycle::Ramped;
|
||||
let mut weights = ObjectiveWeights::default();
|
||||
weights.gamma_gain = 0.20;
|
||||
weights.phase_locking = 0.15;
|
||||
weights.comfort = 0.25;
|
||||
weights.breathing_stability = 0.20;
|
||||
weights.overstimulation = 0.20;
|
||||
Self {
|
||||
id: "mood-arousal",
|
||||
display_name: "Mood & Arousal Regulation (calming-response tuning)",
|
||||
evidence_level: EvidenceLevel::EarlyHuman,
|
||||
claim: "personalized calming-response optimization",
|
||||
envelope: SafetyEnvelope {
|
||||
brightness_cap: 0.30,
|
||||
volume_cap: 0.30,
|
||||
..SafetyEnvelope::conservative()
|
||||
},
|
||||
prior,
|
||||
weights,
|
||||
eligible_states: &[SleepState::QuietWake, SleepState::Drowsy],
|
||||
time_preference: TimePreference::Evening,
|
||||
}
|
||||
}
|
||||
|
||||
/// Home neuro-wellness — safe personalization without treatment claims. The
|
||||
/// most conservative envelope and the shortest sessions; speculative
|
||||
/// evidence, so the claim is explicitly wellness-only.
|
||||
pub fn home_wellness() -> Self {
|
||||
let mut prior = StimulusParameters::prior();
|
||||
prior.brightness_level = 0.20;
|
||||
prior.volume_level = 0.20;
|
||||
prior.duration_minutes = 6.0;
|
||||
Self {
|
||||
id: "home-wellness",
|
||||
display_name: "Home Neuro-Wellness (no treatment claim)",
|
||||
evidence_level: EvidenceLevel::Speculative,
|
||||
claim: "personal neural-rhythm wellness optimization",
|
||||
envelope: SafetyEnvelope {
|
||||
brightness_cap: 0.28,
|
||||
volume_cap: 0.28,
|
||||
max_duration_minutes: 10.0,
|
||||
..SafetyEnvelope::conservative()
|
||||
},
|
||||
prior,
|
||||
weights: ObjectiveWeights::default(),
|
||||
eligible_states: &[SleepState::QuietWake],
|
||||
time_preference: TimePreference::Any,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drug-plus-device trial infrastructure — the strongest near-term use. The
|
||||
/// value is the *governed measurement layer* (RuView state + adherence,
|
||||
/// RuVector response curve, RuFlo protocol/safety/consent/sham log), so the
|
||||
/// claim is about a biomarker-correlated protocol layer, not therapy.
|
||||
pub fn trial_infrastructure() -> Self {
|
||||
Self {
|
||||
id: "trial-infrastructure",
|
||||
display_name: "Drug+Device Trial Infrastructure (governed protocol layer)",
|
||||
evidence_level: EvidenceLevel::StrongInfrastructure,
|
||||
claim: "governed, reproducible entrainment-protocol measurement",
|
||||
envelope: SafetyEnvelope::conservative(),
|
||||
prior: StimulusParameters::prior(),
|
||||
weights: ObjectiveWeights::default(),
|
||||
eligible_states: &[SleepState::QuietWake, SleepState::Drowsy],
|
||||
time_preference: TimePreference::Any,
|
||||
}
|
||||
}
|
||||
|
||||
/// Every built-in program — for catalog UIs and the acceptance test matrix.
|
||||
pub fn catalog() -> Vec<NeuroProgram> {
|
||||
vec![
|
||||
Self::alzheimers_research(),
|
||||
Self::post_stroke_cognition(),
|
||||
Self::sleep_optimization(),
|
||||
Self::attention_working_memory(),
|
||||
Self::mood_arousal(),
|
||||
Self::home_wellness(),
|
||||
Self::trial_infrastructure(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_program_prior_is_inside_its_envelope() {
|
||||
for p in NeuroProgram::catalog() {
|
||||
assert!(
|
||||
p.envelope.contains(&p.prior),
|
||||
"program {} prior must be inside its envelope",
|
||||
p.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_program_claim_is_a_disease_treatment_claim() {
|
||||
let banned = ["treat", "cure", "alzheimer", "stroke recovery cure", "therapy for"];
|
||||
for p in NeuroProgram::catalog() {
|
||||
let claim = p.claim.to_lowercase();
|
||||
for b in banned {
|
||||
assert!(
|
||||
!claim.contains(b),
|
||||
"program {} claim '{}' contains banned term '{}'",
|
||||
p.id,
|
||||
p.claim,
|
||||
b
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objective_weights_are_well_formed() {
|
||||
for p in NeuroProgram::catalog() {
|
||||
let w = &p.weights;
|
||||
for v in [
|
||||
w.gamma_gain,
|
||||
w.phase_locking,
|
||||
w.breathing_stability,
|
||||
w.adherence,
|
||||
w.comfort,
|
||||
w.motion_artifact,
|
||||
w.adverse_event_risk,
|
||||
w.overstimulation,
|
||||
] {
|
||||
assert!((0.0..=1.0).contains(&v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_program_permits_asleep_others_do_not() {
|
||||
assert!(NeuroProgram::sleep_optimization().state_eligible(SleepState::Asleep));
|
||||
assert!(!NeuroProgram::attention_working_memory().state_eligible(SleepState::Asleep));
|
||||
assert!(!NeuroProgram::alzheimers_research().state_eligible(SleepState::Asleep));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_program_caps_brightness_near_dark() {
|
||||
assert!(NeuroProgram::sleep_optimization().envelope.brightness_cap <= 0.10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_ids_are_unique() {
|
||||
let cat = NeuroProgram::catalog();
|
||||
let mut ids: Vec<&str> = cat.iter().map(|p| p.id).collect();
|
||||
ids.sort_unstable();
|
||||
let n = ids.len();
|
||||
ids.dedup();
|
||||
assert_eq!(ids.len(), n);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alzheimers_program_matches_adr250_defaults() {
|
||||
// The default-enroll path must be unchanged (witness stability).
|
||||
let p = NeuroProgram::alzheimers_research();
|
||||
assert_eq!(p.envelope, SafetyEnvelope::conservative());
|
||||
assert_eq!(p.prior, StimulusParameters::prior());
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
use crate::objective::{SafeEntrainmentObjective, ScoreInputs};
|
||||
use crate::optimizer::{BayesianOptimizer, CalibrationPlan, Recommendation};
|
||||
use crate::response::{PersonResponseVector, RuViewState, SessionObservation, SubjectiveReport};
|
||||
use crate::program::NeuroProgram;
|
||||
use crate::response::{PersonResponseVector, RuViewState, SessionObservation, SleepState, SubjectiveReport};
|
||||
use crate::ruvector::{AnonymizedProfile, DriftDetector, DriftStatus, ProfileStore};
|
||||
use crate::safety::{
|
||||
ExclusionCondition, ExclusionScreen, SafetyMonitor, SafetyTick, ScreenOutcome, StopReason,
|
||||
@@ -80,6 +81,10 @@ pub struct RufloGovernor {
|
||||
// ADR-250 §10 item 4: per-person drift detection over the response vector.
|
||||
drift: DriftDetector,
|
||||
drift_status: DriftStatus,
|
||||
// Platform extension: the program this participant is enrolled under
|
||||
// (envelope/prior/objective/state-gating/claim). `None` for the bare
|
||||
// `enroll` path (Alzheimer's defaults), which keeps the pinned witness.
|
||||
program: Option<NeuroProgram>,
|
||||
}
|
||||
|
||||
impl RufloGovernor {
|
||||
@@ -115,9 +120,51 @@ impl RufloGovernor {
|
||||
next_index: 0,
|
||||
drift: DriftDetector::default(),
|
||||
drift_status: DriftStatus::Warmup,
|
||||
program: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Enroll a participant under a [`NeuroProgram`] (the platform path): the
|
||||
/// program supplies the safety envelope, starting prior, and objective
|
||||
/// weighting for this use case. Same fail-closed consent/exclusion gate as
|
||||
/// [`enroll`](Self::enroll). The program's claim is only releasable through
|
||||
/// the acceptance gate (`crate::acceptance`), never directly.
|
||||
pub fn enroll_program(
|
||||
person_id: impl Into<String>,
|
||||
program: NeuroProgram,
|
||||
conditions: &[ExclusionCondition],
|
||||
consent: Consent,
|
||||
) -> Result<Self, GovernanceError> {
|
||||
let mut gov = Self::enroll(person_id, program.envelope, conditions, consent)?;
|
||||
gov.objective = SafeEntrainmentObjective::new(program.weights, program.envelope);
|
||||
gov.versions.protocol_version = format!("adr-250-{}-v0.1", program.id);
|
||||
gov.program = Some(program);
|
||||
Ok(gov)
|
||||
}
|
||||
|
||||
/// The program this participant is enrolled under, if any.
|
||||
pub fn program(&self) -> Option<&NeuroProgram> {
|
||||
self.program.as_ref()
|
||||
}
|
||||
|
||||
/// The program's starting prior, or the ADR-250 40 Hz prior if none.
|
||||
pub fn prior(&self) -> StimulusParameters {
|
||||
self.program
|
||||
.as_ref()
|
||||
.map(|p| p.prior)
|
||||
.unwrap_or_else(StimulusParameters::prior)
|
||||
}
|
||||
|
||||
/// Whether a session in `state` fits the enrolled program's protocol
|
||||
/// (e.g. the sleep program permits `Asleep`). Programs without state
|
||||
/// constraints (the bare path) accept any state.
|
||||
pub fn state_eligible(&self, state: SleepState) -> bool {
|
||||
self.program
|
||||
.as_ref()
|
||||
.map(|p| p.state_eligible(state))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Seed the optimizer from a cohort of anonymized similar responders
|
||||
/// (ADR-250 §10 item 3): the `k` nearest profiles' frequency responses
|
||||
/// enter as **down-weighted pseudo-observations**, shaping where the
|
||||
|
||||
Reference in New Issue
Block a user