Files

269 lines
8.0 KiB
JavaScript

/**
* physiology.js — Fizyoloji Cekirdegi
* ---------------------------------------------------------------
* Bu modul UI'dan TAMAMEN bagimsizdir. Sadece sayilar ve durum
* yonetir. Render katmani buradan okur, buradan yazmaz.
*
* Temel model:
* Kalp -> atim hacmi (SV) x kalp hizi (HR) = kalp debisi (CO)
* CO -> dokuya O2 tasima -> oksijen saturasyonu (SpO2)
* Sinir -> lif hizi x yol uzunlugu -> tepki suresi (ms)
* ---------------------------------------------------------------
*/
(function (root) {
'use strict';
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const lerp = (a, b, t) => a + (b - a) * t;
/** Simulasyon durumu. Tum metrikler burada yasar. */
function createState() {
const st = {
t: 0,
running: true,
speed: 1,
// --- Kalp ---
hr: 72,
hrSet: 72,
sv: 70,
contract: 0,
beatPhase: 0,
beatCount: 0,
lastBP: { sys: 120, dia: 80 },
// --- Dolasim ---
vascularTone: 1.0,
map: 93,
flow: 5.0,
flowRate: 1.0,
// --- Solunum / Oksijen ---
spo2: 98,
paO2: 95,
respRate: 14,
respPhase: 0,
o2Load: 1.0,
// --- Sinir sistemi ---
nerveConduction: 1.0,
neuralTone: 1.0,
responseMs: 0,
lastTarget: null,
spinalLoad: 0,
// --- Senaryo ---
scenario: 'rest',
scenarioT: 0,
// --- Olay gunlugu ---
log: [],
logSeq: 0,
// --- Gecmis (sparkline) ---
history: { hr: [], spo2: [], flow: [], resp: [] },
_histAcc: 0
};
return st;
}
/** Gunluge olay ekle. */
function logEvent(st, level, message) {
st.logSeq++;
st.log.unshift({ id: st.logSeq, t: +st.t.toFixed(1), level, message });
if (st.log.length > 120) st.log.pop();
return st.log[0];
}
/**
* Kalp kasilma dongusu.
* Atim suresi RR = 60 / HR.
* Sistol dongunun ilk ~%38'i; kalp hizi arttikca sistol kisalir.
*/
function stepCardiac(st, dt) {
const hr = clamp(st.hr, 20, 240);
const rr = 60 / hr;
st.beatPhase += dt / rr;
while (st.beatPhase >= 1) {
st.beatPhase -= 1;
st.beatCount++;
onBeat(st);
}
const sysFrac = clamp(0.38 * (1 - (hr - 60) / 400), 0.22, 0.42);
if (st.beatPhase < sysFrac) {
const p = st.beatPhase / sysFrac;
st.contract = Math.pow(Math.sin(p * Math.PI), 0.7);
} else {
st.contract = 0;
}
}
/** Her atimda: atim hacmi ve kan basinci dalgasi. */
function onBeat(st) {
const preload = clamp(1.0 - (st.vascularTone - 1) * 0.15, 0.7, 1.3);
const fill = clamp(st.o2Load * 0.5 + 0.5, 0.6, 1.15);
st.sv = clamp(70 * preload * fill, 35, 130);
const pulsePressure = clamp(st.sv * 0.6 * st.vascularTone, 25, 90);
st.lastBP = {
sys: Math.round(st.map + pulsePressure * 0.6),
dia: Math.round(st.map - pulsePressure * 0.4)
};
}
/** Hemodinamik: CO = HR x SV / 1000, MAP = CO x SVR. */
function stepHemodynamics(st, dt) {
const co = (st.hr * st.sv) / 1000;
st.flow = co;
st.flowRate = lerp(st.flowRate, co / 5.0, clamp(dt * 2.5, 0, 1));
// MAP = CO x SVR. Normal: 5 L/dk x 18.6 = 93 mmHg.
const svr = 18.6 * st.vascularTone;
let mapTarget = co * svr;
// Baroreseptor refleksi: CO cok artinca damarlar genisler,
// MAP lineer degil (CO^0.65) buyur. Egzersizde MAP ~140'ta kalir.
if (co > 5) {
mapTarget = 5 * svr + (Math.pow(co / 5, 0.65) - 1) * 5 * svr * 0.55;
}
mapTarget = clamp(mapTarget, 40, 190);
st.map = lerp(st.map, mapTarget, clamp(dt * 1.2, 0, 1));
}
/**
* Gaz degisimi. Alveoler PO2 -> arteriyel PO2 -> SpO2.
* Oksihemoglobin dissosiyasyon egrisi Hill yaklasimi (n=2.7, P50=26.8).
*/
function stepOxygen(st, dt) {
const rr = clamp(st.respRate, 4, 60);
st.respPhase = (st.respPhase + dt / (60 / rr)) % 1;
const ventFactor = clamp(st.respRate / 14, 0.5, 2.0);
let pao2 = 100 * st.o2Load * (0.85 + 0.15 * Math.min(ventFactor, 1.6));
// V/Q uyumsuzlugu: CO cok dusukse difuzyon bozulur
const vq = clamp(0.7 + 0.3 * Math.min(st.flowRate, 1.5), 0.7, 1.15);
pao2 = clamp(pao2 * vq, 25, 125);
st.paO2 = lerp(st.paO2, pao2, clamp(dt * 0.6, 0, 1));
const p50 = 26.8, n = 2.7;
const sat = clamp(
(Math.pow(st.paO2, n) / (Math.pow(st.paO2, n) + Math.pow(p50, n))) * 100,
20, 99.9
);
st.spo2 = lerp(st.spo2, sat, clamp(dt * 0.9, 0, 1));
}
/** Sinir iletim hizi (goreli): normal 50 m/s referans. */
function nerveVelocity(st) {
return 50 * st.nerveConduction;
}
/**
* Hedefe sinyal gonder; tepki suresini milisaniye cinsinden hesapla.
* 1 goreli yol birimi = 0.45 m; +8 ms sinaptik gecikme.
*/
function fireSignal(st, target) {
const distanceM = (target.pathLength || 1) * 0.45;
const v = nerveVelocity(st);
const latency = (distanceM / v) * 1000 + 8;
const toneFactor = clamp(1 / st.neuralTone, 0.85, 1.25);
st.responseMs = Math.round(latency * toneFactor);
st.lastTarget = target.id;
st.spinalLoad = 1.0;
return {
ms: st.responseMs,
distanceM: +distanceM.toFixed(3),
velocity: +v.toFixed(1),
target
};
}
function stepNeural(st, dt) {
st.spinalLoad = Math.max(0, st.spinalLoad - dt * 0.9);
st.neuralTone = lerp(st.neuralTone, 1.0, clamp(dt * 0.15, 0, 1));
}
function stepHistory(st, dt) {
st._histAcc += dt;
if (st._histAcc < 0.25) return;
st._histAcc = 0;
push(st.history.hr, st.hr);
push(st.history.spo2, st.spo2);
push(st.history.flow, st.flow);
push(st.history.resp, st.respRate);
}
function push(arr, v) {
arr.push(v);
if (arr.length > 120) arr.shift();
}
/** ANA ADIM. dt = gercek saniye; speed ile olceklenir. */
function step(st, dt) {
if (!st.running) return;
const simDt = clamp(dt, 0, 0.1) * st.speed;
st.t += simDt;
st.scenarioT += simDt;
stepCardiac(st, simDt);
stepHemodynamics(st, simDt);
stepOxygen(st, simDt);
stepNeural(st, simDt);
stepHistory(st, simDt);
}
/** Anlik metrik demeti — API ve UI ayni sekli kullanir. */
function snapshot(st) {
return {
t: +st.t.toFixed(2),
running: st.running,
speed: st.speed,
hr: Math.round(st.hr),
sv: Math.round(st.sv),
contract: +st.contract.toFixed(3),
beatPhase: +st.beatPhase.toFixed(3),
beatCount: st.beatCount,
flow: +st.flow.toFixed(2),
flowRate: +st.flowRate.toFixed(2),
map: Math.round(st.map),
bp: st.lastBP,
spo2: +st.spo2.toFixed(1),
paO2: +st.paO2.toFixed(1),
respRate: Math.round(st.respRate),
respPhase: +st.respPhase.toFixed(3),
nerveConduction: +st.nerveConduction.toFixed(2),
neuralTone: +st.neuralTone.toFixed(2),
responseMs: st.responseMs,
lastTarget: st.lastTarget,
scenario: st.scenario
};
}
/** Vucut bolgeleri: sinir yolu uzunluklari (goreli birim). */
const BODY_TARGETS = {
head: { id: 'head', label: 'Bas / Yuz', pathLength: 0.6, type: 'sensory' },
larm: { id: 'larm', label: 'Sol Kol', pathLength: 1.0, type: 'motor' },
rarm: { id: 'rarm', label: 'Sag Kol', pathLength: 1.0, type: 'motor' },
lhand: { id: 'lhand', label: 'Sol El', pathLength: 1.4, type: 'sensory' },
rhand: { id: 'rhand', label: 'Sag El', pathLength: 1.4, type: 'sensory' },
chest: { id: 'chest', label: 'Gogus / Kalp', pathLength: 0.7, type: 'sensory' },
lleg: { id: 'lleg', label: 'Sol Bacak', pathLength: 1.9, type: 'motor' },
rleg: { id: 'rleg', label: 'Sag Bacak', pathLength: 1.9, type: 'motor' },
lfoot: { id: 'lfoot', label: 'Sol Ayak', pathLength: 2.3, type: 'sensory' },
rfoot: { id: 'rfoot', label: 'Sag Ayak', pathLength: 2.3, type: 'sensory' }
};
const api = {
createState, step, snapshot, fireSignal, nerveVelocity, logEvent,
BODY_TARGETS, clamp, lerp
};
if (typeof module !== 'undefined' && module.exports) module.exports = api;
root.NumexPhysiology = api;
})(typeof window !== 'undefined' ? window : globalThis);