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:
Claude
2026-06-10 04:19:19 +00:00
parent 2aac160067
commit d55e3659be
9 changed files with 923 additions and 3 deletions
+1
View File
@@ -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 36).** 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.
+1 -1
View File
@@ -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
+35
View File
@@ -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 36 | 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);
+337
View File
@@ -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);
}
}
}
}
+74
View File
@@ -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.
+372
View File
@@ -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());
}
}
+48 -1
View File
@@ -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