use serde::{Deserialize, Serialize};
use std::f32::consts::TAU;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CircadianPhase {
Active,
Dusk,
Rest,
Dawn,
}
impl CircadianPhase {
pub fn duty_factor(&self) -> f32 {
match self {
CircadianPhase::Active => 1.0,
CircadianPhase::Dawn => 0.5,
CircadianPhase::Dusk => 0.3,
CircadianPhase::Rest => 0.05,
}
}
pub fn allows_learning(&self) -> bool {
matches!(self, CircadianPhase::Active | CircadianPhase::Dawn)
}
pub fn allows_consolidation(&self) -> bool {
matches!(self, CircadianPhase::Rest | CircadianPhase::Dusk)
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct PhaseModulation {
pub velocity: f32,
pub offset: f32,
}
impl PhaseModulation {
pub fn neutral() -> Self {
Self {
velocity: 1.0,
offset: 0.0,
}
}
pub fn accelerate(factor: f32) -> Self {
Self {
velocity: factor.max(0.1),
offset: 0.0,
}
}
pub fn decelerate(factor: f32) -> Self {
Self {
velocity: (1.0 / factor.max(0.1)).min(10.0),
offset: 0.0,
}
}
pub fn nudge_forward(radians: f32) -> Self {
Self {
velocity: 1.0,
offset: radians,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CircadianController {
phase: f32,
period: f32,
state: CircadianPhase,
light_signal: f32,
phase_shift: f32,
coherence: f32,
elapsed: f64,
activity_count: u64,
time_in_phase: f32,
dawn_start: f32,
active_start: f32,
dusk_start: f32,
rest_start: f32,
modulation: PhaseModulation,
compute_latch: Option<bool>,
learn_latch: Option<bool>,
consolidate_latch: Option<bool>,
}
impl CircadianController {
pub fn new(period: f32) -> Self {
Self {
phase: 0.0,
period,
state: CircadianPhase::Rest,
light_signal: 0.0,
phase_shift: 0.0,
coherence: 0.5,
elapsed: 0.0,
activity_count: 0,
time_in_phase: 0.0,
dawn_start: 0.25 * TAU, active_start: 0.33 * TAU, dusk_start: 0.75 * TAU, rest_start: 0.92 * TAU, modulation: PhaseModulation::neutral(),
compute_latch: None,
learn_latch: None,
consolidate_latch: None,
}
}
pub fn fast_cycle(period: f32) -> Self {
let mut ctrl = Self::new(period);
ctrl.dawn_start = 0.0;
ctrl.active_start = 0.2 * TAU;
ctrl.dusk_start = 0.6 * TAU;
ctrl.rest_start = 0.75 * TAU;
ctrl
}
pub fn advance(&mut self, dt: f32) {
let entrainment_rate = 0.1 * dt / self.period;
if self.light_signal > 0.5 {
self.phase_shift += entrainment_rate * (self.light_signal - 0.5);
}
let velocity = self.modulation.velocity.clamp(0.1, 10.0);
let offset = self.modulation.offset;
self.modulation.offset = 0.0;
let delta_phase = TAU * dt * velocity / self.period;
self.phase = (self.phase + delta_phase + self.phase_shift + offset) % TAU;
if self.phase < 0.0 {
self.phase += TAU; }
self.phase_shift *= 0.99;
self.elapsed += dt as f64;
self.time_in_phase += dt;
let new_state = self.compute_phase_state();
if new_state != self.state {
self.state = new_state;
self.time_in_phase = 0.0;
self.activity_count = 0;
self.compute_latch = None;
self.learn_latch = None;
self.consolidate_latch = None;
}
}
fn compute_phase_state(&self) -> CircadianPhase {
let p = self.phase;
if p >= self.rest_start || p < self.dawn_start {
CircadianPhase::Rest
} else if p >= self.dusk_start {
CircadianPhase::Dusk
} else if p >= self.active_start {
CircadianPhase::Active
} else {
CircadianPhase::Dawn
}
}
pub fn receive_light(&mut self, intensity: f32) {
self.light_signal = intensity.clamp(0.0, 1.0);
}
pub fn set_coherence(&mut self, coherence: f32) {
self.coherence = coherence.clamp(0.0, 1.0);
}
pub fn modulate(&mut self, modulation: PhaseModulation) {
self.modulation = modulation;
}
pub fn current_modulation(&self) -> PhaseModulation {
self.modulation
}
#[inline]
pub fn should_compute(&mut self) -> bool {
if let Some(latched) = self.compute_latch {
return latched;
}
let decision = matches!(self.state, CircadianPhase::Active | CircadianPhase::Dawn);
self.compute_latch = Some(decision);
decision
}
#[inline]
pub fn should_learn(&mut self) -> bool {
if let Some(latched) = self.learn_latch {
return latched;
}
let decision = self.state.allows_learning() && self.coherence > 0.3;
self.learn_latch = Some(decision);
decision
}
#[inline]
pub fn should_consolidate(&mut self) -> bool {
if let Some(latched) = self.consolidate_latch {
return latched;
}
let decision = self.state.allows_consolidation();
self.consolidate_latch = Some(decision);
decision
}
#[inline]
pub fn peek_compute(&self) -> bool {
self.compute_latch
.unwrap_or_else(|| matches!(self.state, CircadianPhase::Active | CircadianPhase::Dawn))
}
#[inline]
pub fn peek_learn(&self) -> bool {
self.learn_latch
.unwrap_or_else(|| self.state.allows_learning() && self.coherence > 0.3)
}
#[inline]
pub fn should_react(&self, importance: f32) -> bool {
let threshold = match self.state {
CircadianPhase::Active => 0.1, CircadianPhase::Dawn => 0.3, CircadianPhase::Dusk => 0.5, CircadianPhase::Rest => 0.8, };
importance > threshold && (self.coherence > 0.3 || importance > 0.9)
}
#[inline]
pub fn duty_factor(&self) -> f32 {
self.state.duty_factor()
}
#[inline]
pub fn phase_state(&self) -> CircadianPhase {
self.state
}
#[inline]
pub fn phase_angle(&self) -> f32 {
self.phase
}
#[inline]
pub fn period(&self) -> f32 {
self.period
}
#[inline]
pub fn elapsed(&self) -> f64 {
self.elapsed
}
#[inline]
pub fn record_activity(&mut self) {
self.activity_count += 1;
}
#[inline]
pub fn activity_count(&self) -> u64 {
self.activity_count
}
#[inline]
pub fn time_in_phase(&self) -> f32 {
self.time_in_phase
}
pub fn cost_reduction_factor(&self) -> f32 {
1.0 / self.duty_factor().max(0.01)
}
pub fn reset_to(&mut self, fraction: f32) {
self.phase = fraction.clamp(0.0, 1.0) * TAU;
self.state = self.compute_phase_state();
self.time_in_phase = 0.0;
self.activity_count = 0;
}
}
impl Default for CircadianController {
fn default() -> Self {
Self::new(24.0)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HysteresisTracker {
ticks_above: u32,
required_ticks: u32,
threshold: f32,
triggered: bool,
}
impl HysteresisTracker {
pub fn new(threshold: f32, required_ticks: u32) -> Self {
Self {
ticks_above: 0,
required_ticks: required_ticks.max(1),
threshold,
triggered: false,
}
}
pub fn update(&mut self, value: f32) -> bool {
if value > self.threshold {
self.ticks_above = self.ticks_above.saturating_add(1);
if self.ticks_above >= self.required_ticks {
self.triggered = true;
}
} else {
self.ticks_above = 0;
self.triggered = false;
}
self.triggered
}
pub fn is_triggered(&self) -> bool {
self.triggered
}
pub fn reset(&mut self) {
self.ticks_above = 0;
self.triggered = false;
}
pub fn ticks_above(&self) -> u32 {
self.ticks_above
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetGuardrail {
budget_per_hour: f64,
current_spend: f64,
hours_elapsed: f64,
overspend_reduction: f32,
overspending: bool,
spend_history: Vec<f64>,
max_history: usize,
}
impl BudgetGuardrail {
pub fn new(budget_per_hour: f64, overspend_reduction: f32) -> Self {
Self {
budget_per_hour,
current_spend: 0.0,
hours_elapsed: 0.0,
overspend_reduction: overspend_reduction.clamp(0.0, 1.0),
overspending: false,
spend_history: Vec::with_capacity(24),
max_history: 24,
}
}
pub fn record_spend(&mut self, spend: f64, dt_hours: f64) {
self.current_spend += spend;
self.hours_elapsed += dt_hours;
if self.hours_elapsed >= 1.0 {
if self.spend_history.len() >= self.max_history {
self.spend_history.remove(0);
}
self.spend_history.push(self.current_spend);
self.current_spend = 0.0;
self.hours_elapsed -= 1.0;
}
let projected_spend = self.current_spend / self.hours_elapsed.max(0.001);
self.overspending = projected_spend > self.budget_per_hour;
}
pub fn duty_multiplier(&self) -> f32 {
if self.overspending {
self.overspend_reduction
} else {
1.0
}
}
pub fn is_overspending(&self) -> bool {
self.overspending
}
pub fn current_spend_rate(&self) -> f64 {
if self.hours_elapsed > 0.0 {
self.current_spend / self.hours_elapsed
} else {
0.0
}
}
pub fn utilization(&self) -> f64 {
self.current_spend_rate() / self.budget_per_hour
}
pub fn average_historical_spend(&self) -> f64 {
if self.spend_history.is_empty() {
return 0.0;
}
self.spend_history.iter().sum::<f64>() / self.spend_history.len() as f64
}
pub fn reset(&mut self) {
self.current_spend = 0.0;
self.hours_elapsed = 0.0;
self.overspending = false;
self.spend_history.clear();
}
}
impl Default for BudgetGuardrail {
fn default() -> Self {
Self::new(1000.0, 0.5) }
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NervousSystemMetrics {
pub total_ticks: u64,
pub active_ticks: u64,
pub total_spikes: u64,
pub total_energy: f64,
pub baseline_spikes_per_hour: f64,
decision_latencies: Vec<u64>,
max_latencies: usize,
pub memory_writes: u64,
pub meaningful_events: u64,
}
impl NervousSystemMetrics {
pub fn new(baseline_spikes_per_hour: f64) -> Self {
Self {
total_ticks: 0,
active_ticks: 0,
total_spikes: 0,
total_energy: 0.0,
baseline_spikes_per_hour,
decision_latencies: Vec::with_capacity(1000),
max_latencies: 1000,
memory_writes: 0,
meaningful_events: 0,
}
}
pub fn record_tick(&mut self, active: bool, spikes: u64, energy: f64) {
self.total_ticks += 1;
if active {
self.active_ticks += 1;
}
self.total_spikes += spikes;
self.total_energy += energy;
}
pub fn record_memory_op(&mut self, writes: u64, meaningful: bool) {
self.memory_writes += writes;
if meaningful {
self.meaningful_events += 1;
}
}
pub fn record_decision(&mut self, latency_us: u64) {
if self.decision_latencies.len() >= self.max_latencies {
self.decision_latencies.remove(0);
}
self.decision_latencies.push(latency_us);
}
pub fn silence_ratio(&self) -> f64 {
if self.total_ticks == 0 {
return 1.0;
}
1.0 - (self.active_ticks as f64 / self.total_ticks as f64)
}
pub fn ttd_p50(&self) -> Option<u64> {
self.percentile(0.5)
}
pub fn ttd_p95(&self) -> Option<u64> {
self.percentile(0.95)
}
fn percentile(&self, p: f64) -> Option<u64> {
if self.decision_latencies.is_empty() {
return None;
}
let mut sorted = self.decision_latencies.clone();
sorted.sort_unstable();
let idx = ((sorted.len() as f64 * p) as usize).min(sorted.len() - 1);
Some(sorted[idx])
}
pub fn energy_per_spike(&self) -> f64 {
if self.total_spikes == 0 {
return 0.0;
}
self.total_energy / self.total_spikes as f64
}
pub fn calmness_index(&self, hours_elapsed: f64) -> f64 {
if hours_elapsed <= 0.0 || self.baseline_spikes_per_hour <= 0.0 {
return 1.0;
}
let spikes_per_hour = self.total_spikes as f64 / hours_elapsed;
(-spikes_per_hour / self.baseline_spikes_per_hour).exp()
}
pub fn write_amplification(&self) -> f64 {
if self.meaningful_events == 0 {
return 0.0;
}
self.memory_writes as f64 / self.meaningful_events as f64
}
pub fn ttd_exceeds_budget(&self, budget_us: u64) -> bool {
self.ttd_p95().map(|p95| p95 > budget_us).unwrap_or(false)
}
pub fn scorecard(&self, hours_elapsed: f64) -> NervousSystemScorecard {
NervousSystemScorecard {
silence_ratio: self.silence_ratio(),
ttd_p50_us: self.ttd_p50(),
ttd_p95_us: self.ttd_p95(),
energy_per_spike: self.energy_per_spike(),
calmness_index: self.calmness_index(hours_elapsed),
write_amplification: self.write_amplification(),
total_ticks: self.total_ticks,
total_spikes: self.total_spikes,
}
}
pub fn reset(&mut self) {
self.total_ticks = 0;
self.active_ticks = 0;
self.total_spikes = 0;
self.total_energy = 0.0;
self.decision_latencies.clear();
self.memory_writes = 0;
self.meaningful_events = 0;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NervousSystemScorecard {
pub silence_ratio: f64,
pub ttd_p50_us: Option<u64>,
pub ttd_p95_us: Option<u64>,
pub energy_per_spike: f64,
pub calmness_index: f64,
pub write_amplification: f64,
pub total_ticks: u64,
pub total_spikes: u64,
}
impl NervousSystemScorecard {
pub fn is_healthy(&self, targets: &ScorecardTargets) -> bool {
self.silence_ratio >= targets.min_silence_ratio
&& self
.ttd_p95_us
.map(|p95| p95 <= targets.max_ttd_p95_us)
.unwrap_or(true)
&& self.energy_per_spike <= targets.max_energy_per_spike
&& self.write_amplification <= targets.max_write_amplification
}
pub fn health_score(&self, targets: &ScorecardTargets) -> f64 {
let mut score = 0.0;
let mut count = 0.0;
score += (self.silence_ratio / targets.min_silence_ratio).min(1.0);
count += 1.0;
if let Some(p95) = self.ttd_p95_us {
score += (targets.max_ttd_p95_us as f64 / p95 as f64).min(1.0);
count += 1.0;
}
if self.energy_per_spike > 0.0 {
score += (targets.max_energy_per_spike / self.energy_per_spike).min(1.0);
count += 1.0;
}
if self.write_amplification > 0.0 {
score += (targets.max_write_amplification / self.write_amplification).min(1.0);
count += 1.0;
}
score += self.calmness_index;
count += 1.0;
score / count
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScorecardTargets {
pub min_silence_ratio: f64,
pub max_ttd_p95_us: u64,
pub max_energy_per_spike: f64,
pub max_write_amplification: f64,
}
impl Default for ScorecardTargets {
fn default() -> Self {
Self {
min_silence_ratio: 0.7, max_ttd_p95_us: 10_000, max_energy_per_spike: 100.0, max_write_amplification: 3.0, }
}
}
#[derive(Debug, Clone)]
pub struct CircadianScheduler<T> {
controller: CircadianController,
pending: Vec<T>,
max_pending: usize,
}
impl<T> CircadianScheduler<T> {
pub fn new(period: f32, max_pending: usize) -> Self {
Self {
controller: CircadianController::new(period),
pending: Vec::with_capacity(max_pending.min(1000)),
max_pending,
}
}
pub fn submit<F>(&mut self, task: T, importance: f32, execute: F) -> bool
where
F: FnOnce(T),
{
if self.controller.should_react(importance) {
execute(task);
self.controller.record_activity();
true
} else if self.pending.len() < self.max_pending {
self.pending.push(task);
false
} else {
false
}
}
pub fn advance<F>(&mut self, dt: f32, mut execute: F)
where
F: FnMut(T),
{
self.controller.advance(dt);
if self.controller.should_compute() && !self.pending.is_empty() {
let batch_size = (self.pending.len() as f32 * self.controller.duty_factor()) as usize;
let batch_size = batch_size.max(1).min(self.pending.len());
for _ in 0..batch_size {
if let Some(task) = self.pending.pop() {
execute(task);
self.controller.record_activity();
}
}
}
}
pub fn controller(&self) -> &CircadianController {
&self.controller
}
pub fn controller_mut(&mut self) -> &mut CircadianController {
&mut self.controller
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phase_transitions() {
let mut clock = CircadianController::new(24.0);
assert_eq!(clock.phase_state(), CircadianPhase::Rest);
clock.advance(7.0);
assert_eq!(clock.phase_state(), CircadianPhase::Dawn);
clock.advance(3.0);
assert_eq!(clock.phase_state(), CircadianPhase::Active);
clock.advance(9.0);
assert_eq!(clock.phase_state(), CircadianPhase::Dusk);
clock.advance(5.0);
assert_eq!(clock.phase_state(), CircadianPhase::Rest);
}
#[test]
fn test_duty_factors() {
assert_eq!(CircadianPhase::Active.duty_factor(), 1.0);
assert_eq!(CircadianPhase::Dawn.duty_factor(), 0.5);
assert_eq!(CircadianPhase::Dusk.duty_factor(), 0.3);
assert_eq!(CircadianPhase::Rest.duty_factor(), 0.05);
}
#[test]
fn test_gating_logic() {
let mut clock = CircadianController::new(24.0);
clock.set_coherence(0.8);
assert!(!clock.should_compute());
assert!(!clock.should_learn());
assert!(clock.should_consolidate());
assert!(!clock.should_react(0.5));
assert!(clock.should_react(0.9));
clock.advance(10.0);
assert!(clock.should_compute());
assert!(clock.should_learn());
assert!(!clock.should_consolidate());
assert!(clock.should_react(0.2));
}
#[test]
fn test_entrainment() {
let mut clock1 = CircadianController::new(24.0);
let mut clock2 = CircadianController::new(24.0);
clock2.receive_light(1.0);
for _ in 0..10 {
clock1.advance(1.0);
clock2.advance(1.0);
}
assert!(clock2.phase_angle() > clock1.phase_angle());
}
#[test]
fn test_cost_reduction() {
let mut clock = CircadianController::new(24.0);
assert!(clock.cost_reduction_factor() > 10.0);
clock.advance(10.0);
assert!((clock.cost_reduction_factor() - 1.0).abs() < 0.01);
}
#[test]
fn test_scheduler() {
let mut scheduler: CircadianScheduler<u32> = CircadianScheduler::new(24.0, 100);
let mut executed = Vec::new();
let immediate = scheduler.submit(1, 0.3, |t| executed.push(t));
assert!(!immediate);
assert_eq!(scheduler.pending_count(), 1);
let immediate = scheduler.submit(2, 0.95, |t| executed.push(t));
assert!(immediate);
assert_eq!(executed, vec![2]);
scheduler.advance(10.0, |t| executed.push(t));
assert!(executed.contains(&1));
}
#[test]
fn test_fast_cycle() {
let clock = CircadianController::fast_cycle(1.0);
assert_eq!(clock.period(), 1.0);
let mut c = clock.clone();
let mut phases_seen = std::collections::HashSet::new();
for i in 0..100 {
c.advance(0.01);
phases_seen.insert(c.phase_state());
}
assert_eq!(phases_seen.len(), 4);
}
}