organpool 0.1.0

Organ physics substrate — cardiac pacemaker simulation with autonomic modulation, integer-only ion channel dynamics, and real-time vital sign diagnostics
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
//! Cardiac conduction pipeline — autonomous heart engine.
//!
//! The heart runs its own thread. No tick(). No external clock.
//! The heart has its own internal chemical environment with metabolism.
//! External sources (brain, bloodstream) inject chemicals into the heart's
//! environment. Enzymes metabolize them back toward resting baselines.
//! If nothing injects, the heart beats at its intrinsic rate.
//!
//! ```text
//! Brain injects ──→ [mpsc channel] ──→ Heart's chemical pool (internal)
//!                                           │ metabolism (decay)
//!//! SA Node → AV Node → Conduction → Myocardium → BeatEvent channel
//! ```
//!
//! A denervated heart (no injections) beats at intrinsic rate with resting
//! vagal tone — exactly like a transplanted human heart.

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};

use crate::cardiac::{CardiacConfig, CardiacZone};
use crate::vitals::{BeatEvent, CardiacRhythm, CardiacVitals};

/// Which chemical to inject.
#[derive(Clone, Copy, Debug)]
pub enum Chemical {
    /// Norepinephrine — sympathetic, accelerates.
    NE,
    /// Acetylcholine — parasympathetic, decelerates.
    ACh,
    /// Cortisol — hormonal, amplifies NE sensitivity.
    Cortisol,
}

/// A chemical injection event sent to the heart.
#[derive(Clone, Copy, Debug)]
pub struct ChemicalInjection {
    pub chemical: Chemical,
    /// Amount to add to the local pool (saturating at 255).
    pub amount: u8,
}

/// The heart's internal chemical environment.
///
/// This is entirely owned by the heart thread. Nothing external reads or
/// writes it directly. External sources inject via channel; the heart
/// metabolizes internally.
///
/// Resting baselines reflect real physiology:
/// - NE baseline ~0: sympathetic tone is low at rest (heart is vagally dominated)
/// - ACh baseline ~30: vagal tone is real — the resting heart is actively
///   slowed by parasympathetic activity. Denervated hearts beat faster
///   than innervated ones (~100 BPM vs ~70 BPM) because vagal brake is removed.
/// - Cortisol baseline ~10: low circadian cortisol at rest
pub struct ChemicalPool {
    /// Current local NE concentration (0-255).
    pub ne: u8,
    /// Current local ACh concentration (0-255).
    pub ach: u8,
    /// Current local cortisol concentration (0-255).
    pub cortisol: u8,

    /// Resting NE baseline — metabolism decays toward this.
    pub ne_baseline: u8,
    /// Resting ACh baseline (vagal tone).
    pub ach_baseline: u8,
    /// Resting cortisol baseline.
    pub cortisol_baseline: u8,

    /// NE half-life in microseconds. ~2-3 seconds for synaptic NE (MAO/COMT).
    pub ne_halflife_us: u64,
    /// ACh half-life in microseconds. ~1ms for synaptic ACh (acetylcholinesterase
    /// is extremely fast), but we model the tissue-level effect which lingers ~500ms.
    pub ach_halflife_us: u64,
    /// Cortisol half-life in microseconds. ~60-90 minutes in blood, but we
    /// model the local tissue effect — ~10 seconds for cardiac sensitivity.
    pub cortisol_halflife_us: u64,

    /// Last time metabolism was applied.
    last_metabolism: Instant,
}

impl ChemicalPool {
    fn new(now: Instant) -> Self {
        Self {
            ne: 0,
            ach: 30,       // vagal tone at rest
            cortisol: 10,  // low circadian cortisol

            ne_baseline: 0,
            ach_baseline: 30,
            cortisol_baseline: 10,

            ne_halflife_us: 2_500_000,     // 2.5 seconds
            ach_halflife_us: 500_000,      // 500ms tissue effect
            cortisol_halflife_us: 10_000_000, // 10 seconds local effect

            last_metabolism: now,
        }
    }

    /// Inject a chemical into the pool. Saturates at 255.
    fn inject(&mut self, chemical: Chemical, amount: u8) {
        match chemical {
            Chemical::NE => self.ne = self.ne.saturating_add(amount),
            Chemical::ACh => self.ach = self.ach.saturating_add(amount),
            Chemical::Cortisol => self.cortisol = self.cortisol.saturating_add(amount),
        }
    }

    /// Metabolize: decay each chemical toward its baseline.
    ///
    /// Uses exponential decay: each half-life, the distance from baseline halves.
    /// Only applies when enough time has accumulated (10ms minimum) so integer
    /// arithmetic produces meaningful fractions.
    ///
    /// Cross-metabolism: cortisol inhibits COMT, extending NE half-life.
    /// `cortisol_ne_protection` controls the strength (0=none, 255=doubles halflife).
    fn metabolize(&mut self, now: Instant, cortisol_ne_protection: u8) {
        let elapsed_us = now.duration_since(self.last_metabolism).as_micros() as u64;
        // Metabolize in ≥250ms chunks so fractional decay produces meaningful
        // integer results for u8 distances. At 250ms with a 2.5s half-life,
        // decay is ~6.9% of distance — well above truncation threshold for
        // any distance ≥ 2. Shorter intervals (e.g. 10ms) truncate to 0 for
        // most u8 distances, making different half-lives indistinguishable.
        if elapsed_us < 250_000 {
            return;
        }
        self.last_metabolism = now;

        // Cortisol extends NE half-life by inhibiting COMT.
        // At protection=255 and cortisol=255: half-life doubled.
        // effective = base × (256 + cortisol × protection / 255) / 256
        let ne_halflife = {
            let extension = (self.cortisol as u64 * cortisol_ne_protection as u64) / 255;
            (self.ne_halflife_us * (256 + extension)) / 256
        };

        self.ne = decay_toward(self.ne, self.ne_baseline, elapsed_us, ne_halflife);
        self.ach = decay_toward(self.ach, self.ach_baseline, elapsed_us, self.ach_halflife_us);
        self.cortisol = decay_toward(
            self.cortisol,
            self.cortisol_baseline,
            elapsed_us,
            self.cortisol_halflife_us,
        );
    }
}

/// Exponential decay toward a baseline, integer-only.
///
/// `current` decays toward `baseline` with the given half-life.
/// After `halflife_us` microseconds, the distance from baseline halves.
///
/// For full half-life periods: shift right (halve distance).
/// For fractional remainder: linear approximation using ln(2) ≈ 693/1000.
///
/// Metabolism is batched in ≥250ms chunks so that the fractional decay
/// computation produces meaningful integer results for u8 distances.
/// Shorter intervals would truncate to 0 for most distances, creating
/// a half-life-independent decay rate from the `max(1)` floor.
fn decay_toward(current: u8, baseline: u8, elapsed_us: u64, halflife_us: u64) -> u8 {
    if current == baseline || halflife_us == 0 {
        return current;
    }

    let distance = if current > baseline {
        (current - baseline) as u64
    } else {
        (baseline - current) as u64
    };

    // Full half-lives: each halves the distance
    let full_halflives = elapsed_us / halflife_us;
    let remainder_us = elapsed_us % halflife_us;

    let mut remaining_distance = if full_halflives >= 8 {
        0 // < 0.4% remains — snap to baseline
    } else {
        distance >> full_halflives
    };

    // Fractional remainder: linear approximation of exponential decay
    // decay_amount = distance * (1 - e^(-dt*ln2/halflife))
    //             ≈ distance * dt * ln2 / halflife  (for small dt)
    //             = distance * dt * 693 / (halflife * 1000)
    if remaining_distance > 0 && remainder_us > 0 {
        let decay_amount = (remaining_distance * remainder_us * 693) / (halflife_us * 1000);
        if decay_amount > 0 {
            remaining_distance = remaining_distance.saturating_sub(decay_amount);
        }
    }

    if current > baseline {
        baseline.saturating_add(remaining_distance.min(255) as u8)
    } else {
        baseline.saturating_sub(remaining_distance.min(255) as u8)
    }
}

/// Handle to a running heart. Returned by `CardiacPipeline::start()`.
///
/// The heart runs autonomously in its own thread. You interact with it by:
/// - Injecting chemicals via `inject_ne()`, `inject_ach()`, `inject_cortisol()`
/// - Receiving `BeatEvent`s from `beats`
/// - Calling `stop()` to shut it down
///
/// If you stop injecting, chemicals decay to baseline and the heart returns
/// to its intrinsic rhythm. A denervated heart (no injections ever) beats
/// at ~100 BPM because vagal tone provides the only resting brake and the
/// ACh baseline (30) applies a modest deceleration.
pub struct HeartHandle {
    /// Channel for injecting chemicals into the heart's environment.
    injector: Sender<ChemicalInjection>,
    /// Receiver for beat events. Each myocardium contraction sends one.
    pub beats: Receiver<BeatEvent>,
    /// Signal to stop the heart thread.
    alive: Arc<AtomicBool>,
    /// Join handle for the heart thread.
    thread: Option<JoinHandle<HeartSnapshot>>,
}

impl HeartHandle {
    /// Inject norepinephrine into the heart's chemical environment.
    /// The amount is added to the current local concentration (saturating).
    /// NE will decay back toward baseline (~0) with a half-life of ~2.5s.
    pub fn inject_ne(&self, amount: u8) {
        let _ = self.injector.send(ChemicalInjection {
            chemical: Chemical::NE,
            amount,
        });
    }

    /// Inject acetylcholine into the heart's chemical environment.
    /// ACh decays quickly (half-life ~500ms) back toward vagal tone baseline (~30).
    pub fn inject_ach(&self, amount: u8) {
        let _ = self.injector.send(ChemicalInjection {
            chemical: Chemical::ACh,
            amount,
        });
    }

    /// Inject cortisol into the heart's chemical environment.
    /// Cortisol decays slowly (half-life ~10s) back toward baseline (~10).
    pub fn inject_cortisol(&self, amount: u8) {
        let _ = self.injector.send(ChemicalInjection {
            chemical: Chemical::Cortisol,
            amount,
        });
    }

    /// Check if the heart is still running.
    pub fn is_alive(&self) -> bool {
        self.alive.load(Ordering::Relaxed)
    }

    /// Stop the heart and wait for the thread to finish.
    /// Returns a snapshot of the heart's final state.
    pub fn stop(mut self) -> HeartSnapshot {
        self.alive.store(false, Ordering::Relaxed);
        if let Some(handle) = self.thread.take() {
            handle.join().expect("heart thread panicked")
        } else {
            HeartSnapshot {
                beat_count: 0,
                last_bpm: 0,
                last_rhythm: CardiacRhythm::Asystole,
                last_ibi_us: 0,
                final_ne: 0,
                final_ach: 0,
                final_cortisol: 0,
            }
        }
    }
}

impl Drop for HeartHandle {
    fn drop(&mut self) {
        self.alive.store(false, Ordering::Relaxed);
    }
}

/// Snapshot of heart state at shutdown.
#[derive(Clone, Debug)]
pub struct HeartSnapshot {
    pub beat_count: u64,
    pub last_bpm: u16,
    pub last_rhythm: CardiacRhythm,
    pub last_ibi_us: u64,
    /// Final NE concentration in the heart's pool.
    pub final_ne: u8,
    /// Final ACh concentration.
    pub final_ach: u8,
    /// Final cortisol concentration.
    pub final_cortisol: u8,
}

/// The cardiac conduction pipeline.
///
/// This is a launcher — call `start()` to spawn the heart thread.
/// The heart then runs autonomously until `stop()` is called on the handle.
pub struct CardiacPipeline;

impl CardiacPipeline {
    /// Start the heart with default configuration.
    pub fn start() -> HeartHandle {
        Self::start_with_config(CardiacConfig::default())
    }

    /// Start the heart with custom configuration.
    pub fn start_with_config(config: CardiacConfig) -> HeartHandle {
        let alive = Arc::new(AtomicBool::new(true));
        let (inject_tx, inject_rx) = std::sync::mpsc::channel();
        let (beat_tx, beat_rx) = std::sync::mpsc::channel();

        let alive_clone = Arc::clone(&alive);

        let thread = thread::Builder::new()
            .name("cardiac".into())
            .spawn(move || heart_loop(config, inject_rx, alive_clone, beat_tx))
            .expect("failed to spawn cardiac thread");

        HeartHandle {
            injector: inject_tx,
            beats: beat_rx,
            alive,
            thread: Some(thread),
        }
    }
}

/// The heart's autonomous run loop.
///
/// This is the core physics engine. It owns its chemical environment,
/// metabolizes chemicals internally, and runs zone physics continuously.
/// External injections arrive via channel and are drained each iteration.
fn heart_loop(
    config: CardiacConfig,
    inject_rx: Receiver<ChemicalInjection>,
    alive: Arc<AtomicBool>,
    beat_tx: Sender<BeatEvent>,
) -> HeartSnapshot {
    let now = Instant::now();

    let mut sa_node = CardiacZone::new(&config.sa_node, now);
    let mut av_node = CardiacZone::new(&config.av_node, now);
    let mut conduction = CardiacZone::new(&config.conduction, now);
    let mut myocardium = CardiacZone::new(&config.myocardium, now);
    let mut vitals = CardiacVitals::new();
    let mut pool = ChemicalPool::new(now);

    let sleep_duration = Duration::from_micros(100);

    while alive.load(Ordering::Relaxed) {
        let now = Instant::now();

        // Drain all pending injections from the channel
        while let Ok(injection) = inject_rx.try_recv() {
            pool.inject(injection.chemical, injection.amount);
        }

        // Metabolize: decay chemicals toward baselines (with cross-metabolism)
        pool.metabolize(now, config.metabolism.cortisol_ne_protection);

        // Apply current chemical environment to SA node (with receptor antagonism)
        sa_node.apply_modulation(pool.ne, pool.ach, pool.cortisol, &config.metabolism);

        // Advance zone physics with gap junction coupling
        let sa_fired = sa_node.update(now);
        if sa_fired {
            av_node.trigger(now);
            // SA→AV gap junction: depolarize AV node
            av_node.electrotonic_depolarize(config.gap_sa_av.strength);
        }

        let av_fired = av_node.update(now);
        if av_fired {
            conduction.trigger(now);
            // AV→Conduction gap junction: depolarize conduction system
            conduction.electrotonic_depolarize(config.gap_av_cond.strength);
            // Retrograde: AV→SA (half strength)
            if config.gap_av_cond.retrograde {
                sa_node.electrotonic_depolarize(config.gap_av_cond.strength / 2);
            }
        }

        let cond_fired = conduction.update(now);
        if cond_fired {
            myocardium.trigger(now);
            // Conduction→Myocardium gap junction: depolarize myocardium
            myocardium.electrotonic_depolarize(config.gap_cond_myo.strength);
            // Retrograde: Conduction→AV (half strength)
            if config.gap_cond_myo.retrograde {
                av_node.electrotonic_depolarize(config.gap_cond_myo.strength / 2);
            }
        }

        let myo_fired = myocardium.update(now);
        if myo_fired {
            let beat = vitals.record_beat(now);
            let _ = beat_tx.send(beat);
            // Retrograde: Myocardium→Conduction (half strength)
            if config.gap_cond_myo.retrograde {
                conduction.electrotonic_depolarize(config.gap_cond_myo.strength / 2);
            }
        }

        thread::sleep(sleep_duration);
    }

    let now = Instant::now();
    HeartSnapshot {
        beat_count: vitals.beat_count,
        last_bpm: vitals.bpm(),
        last_rhythm: vitals.classify(now),
        last_ibi_us: vitals.ibi_mean_us(),
        final_ne: pool.ne,
        final_ach: pool.ach,
        final_cortisol: pool.cortisol,
    }
}