Files
medicallib/src/Heart.cpp
T
google-labs-jules[bot] ede2dee772 Feat: Implement interconnected organ simulation
This commit refactors the organ simulation to enable dynamic interactions between organs, replacing the previous "faked" or hardcoded connections.

Key changes include:
- Major Refactoring: Changed the `Organ::update` method signature to `update(Patient& patient, double deltaTime_s)`, allowing organs to access the shared patient state and other organs. This was propagated to all organ classes.
- Blood Chemistry Model: Introduced a central `Blood` struct in the `Patient` model to track shared resources like oxygen, CO2, glucose, and toxins.
- Organ System Interconnections:
    - Lungs & Brain: Lungs now perform gas exchange affecting the blood. The brain consumes O2, produces CO2, and its GCS is affected by hypoxia/hypercapnia.
    - Liver-Gallbladder: Gallbladder now receives bile directly from the liver's production rate.
    - Digestive System: Stomach passes chyme to the intestines, which absorb glucose into the blood. The pancreas responds to blood glucose changes.
    - Renal System: Kidneys' GFR is now influenced by the heart's aortic pressure, and they produce urine that fills the bladder directly.
    - Cardiovascular & Neurological: The heart rate responds to hypoxia, and the brain uses live aortic pressure from the heart.
- Comprehensive Test Scenario: Updated the main example to include a 60-second simulation with a meal and a lung injury event to verify the new interconnected system.

This creates a more realistic and scalable physiological simulation framework where organ behaviors are emergent from their interactions.
2025-08-20 08:30:53 +00:00

206 lines
8.7 KiB
C++

#include "MedicalLib/Heart.h"
#include "MedicalLib/Patient.h"
#include <random>
#include <algorithm>
#include <sstream>
#include <vector>
#include <string>
#include <cmath>
#include <numeric>
#include <deque>
// Helper for Gaussian function to model EKG waves
static double gaussian(double x, double mu, double sigma) {
return exp(-0.5 * pow((x - mu) / sigma, 2));
}
// Helper function for random fluctuations
static double getFluctuation(double stddev) {
static std::random_device rd;
static std::mt19937 gen(rd());
std::normal_distribution<> d(0, stddev);
return d(gen);
}
Heart::Heart(int id, int numLeads)
: Organ(id, "Heart"),
heartRate(75.0),
measuredHeartRate(75.0),
numLeads(numLeads),
ekgHistorySize(200),
totalTime_s(0.0),
cardiacCyclePosition_s(0.0),
lastRPeakTime_s(-1.0),
rPeakDetectedInCycle(false),
ejectionFraction(0.55) {
// Initialize Chambers
leftAtrium.name = "Left Atrium";
rightAtrium.name = "Right Atrium";
leftVentricle.name = "Left Ventricle";
rightVentricle.name = "Right Ventricle";
// Initialize Valves
mitralValve.name = "Mitral Valve";
tricuspidValve.name = "Tricuspid Valve";
aorticValve.name = "Aortic Valve";
pulmonaryValve.name = "Pulmonary Valve";
const std::vector<std::string> allLeadNames = {
"I", "II", "III", "aVR", "aVL", "aVF",
"V1", "V2", "V3", "V4", "V5", "V6"
};
int leadsToCreate = std::min((int)allLeadNames.size(), numLeads);
for (int i = 0; i < leadsToCreate; ++i) {
leadNames.push_back(allLeadNames[i]);
ekgData[allLeadNames[i]] = std::deque<double>();
}
}
void Heart::update(Patient& patient, double deltaTime_s) {
// --- Electrical Simulation Update ---
totalTime_s += deltaTime_s;
// Compensatory response to hypoxia
double o2_saturation = patient.blood.oxygenSaturation;
double targetHeartRate = 75.0;
if (o2_saturation < 90.0) {
targetHeartRate = 75.0 + (90.0 - o2_saturation) * 2.0; // Increase HR as SpO2 drops
}
// Move current heart rate towards the target
heartRate += (targetHeartRate - heartRate) * 0.1 * deltaTime_s;
heartRate += getFluctuation(0.01); // Slow variation in underlying rate
heartRate = std::max(60.0, std::min(heartRate, 140.0));
double cycleDuration_s = 60.0 / heartRate;
double oldCyclePosition = cardiacCyclePosition_s;
cardiacCyclePosition_s += deltaTime_s;
// R-peak detection for measured heart rate
double rPeakCycleTime = 0.22 * cycleDuration_s;
if (oldCyclePosition < rPeakCycleTime && cardiacCyclePosition_s >= rPeakCycleTime && !rPeakDetectedInCycle) {
if (lastRPeakTime_s > 0) {
measuredHeartRate = 60.0 / (totalTime_s - lastRPeakTime_s);
}
lastRPeakTime_s = totalTime_s;
rPeakDetectedInCycle = true;
}
if (cardiacCyclePosition_s > cycleDuration_s) {
cardiacCyclePosition_s -= cycleDuration_s;
rPeakDetectedInCycle = false;
}
double timeInCycle = cardiacCyclePosition_s / cycleDuration_s;
double baseVoltage = simulateEkgWaveform(timeInCycle);
for(const auto& leadName : leadNames) {
double leadModifier = 1.0 - (std::distance(leadNames.begin(), std::find(leadNames.begin(), leadNames.end(), leadName)) * 0.1);
ekgData[leadName].push_front(baseVoltage * leadModifier);
if (ekgData[leadName].size() > ekgHistorySize) ekgData[leadName].pop_back();
}
// --- Mechanical Simulation Update ---
// Define pressures outside the heart
double venousPressure = 5.0; // CVP
double pulmonaryArteryPressure = 20.0;
double aorticPressure = getAorticPressure();
// Determine chamber states based on EKG-timed cycle
if (timeInCycle >= 0.0 && timeInCycle < 0.15) { // Atrial Systole
leftAtrium.state = ChamberState::SYSTOLE;
rightAtrium.state = ChamberState::SYSTOLE;
// End of ventricular filling, capture EDV
if(oldCyclePosition < 0.15 && timeInCycle >= 0.15) {
leftVentricle.endDiastolicVolume_mL = leftVentricle.volume_mL;
}
} else {
leftAtrium.state = ChamberState::DIASTOLE;
rightAtrium.state = ChamberState::DIASTOLE;
}
if (timeInCycle >= 0.20 && timeInCycle < 0.5) { // Ventricular Systole
leftVentricle.state = ChamberState::SYSTOLE;
rightVentricle.state = ChamberState::SYSTOLE;
// End of ventricular ejection, capture ESV and calculate EF
if(oldCyclePosition < 0.5 && timeInCycle >= 0.5) {
leftVentricle.endSystolicVolume_mL = leftVentricle.volume_mL;
if (leftVentricle.endDiastolicVolume_mL > 0) {
ejectionFraction = (leftVentricle.endDiastolicVolume_mL - leftVentricle.endSystolicVolume_mL) / leftVentricle.endDiastolicVolume_mL;
}
}
} else {
leftVentricle.state = ChamberState::DIASTOLE;
rightVentricle.state = ChamberState::DIASTOLE;
}
// Update chamber pressures based on state (very simplified model)
leftAtrium.pressure_mmHg = (leftAtrium.state == ChamberState::SYSTOLE) ? 10.0 : 5.0;
rightAtrium.pressure_mmHg = (rightAtrium.state == ChamberState::SYSTOLE) ? 7.0 : 2.0;
leftVentricle.pressure_mmHg = (leftVentricle.state == ChamberState::SYSTOLE) ? 125.0 * sin((timeInCycle - 0.2) / 0.3 * M_PI) : 5.0;
rightVentricle.pressure_mmHg = (rightVentricle.state == ChamberState::SYSTOLE) ? 25.0 * sin((timeInCycle - 0.2) / 0.3 * M_PI) : 2.0;
// Update Valve Status
tricuspidValve.status = (rightAtrium.pressure_mmHg > rightVentricle.pressure_mmHg) ? ValveStatus::OPEN : ValveStatus::CLOSED;
mitralValve.status = (leftAtrium.pressure_mmHg > leftVentricle.pressure_mmHg) ? ValveStatus::OPEN : ValveStatus::CLOSED;
pulmonaryValve.status = (rightVentricle.pressure_mmHg > pulmonaryArteryPressure) ? ValveStatus::OPEN : ValveStatus::CLOSED;
aorticValve.status = (leftVentricle.pressure_mmHg > aorticPressure) ? ValveStatus::OPEN : ValveStatus::CLOSED;
// Update Chamber Volumes (simplified blood flow)
double flowRate = 500.0 * deltaTime_s; // mL/s
if (mitralValve.status == ValveStatus::OPEN) leftVentricle.volume_mL += flowRate;
if (tricuspidValve.status == ValveStatus::OPEN) rightVentricle.volume_mL += flowRate;
if (aorticValve.status == ValveStatus::OPEN) leftVentricle.volume_mL -= flowRate * 1.5;
if (pulmonaryValve.status == ValveStatus::OPEN) rightVentricle.volume_mL -= flowRate * 1.5;
// Clamp volumes to realistic values
leftVentricle.volume_mL = std::max(40.0, std::min(leftVentricle.volume_mL, 130.0));
rightVentricle.volume_mL = std::max(40.0, std::min(rightVentricle.volume_mL, 130.0));
}
double Heart::simulateEkgWaveform(double timeInCycle) {
double p_time = 0.1, q_time = 0.2, r_time = 0.22, s_time = 0.24, t_time = 0.4;
double p_amp = 0.15, q_amp = -0.1, r_amp = 1.0, s_amp = -0.25, t_amp = 0.3;
double p_sigma = 0.04, qrs_sigma = 0.02, t_sigma = 0.06;
double voltage = 0.0;
voltage += p_amp * gaussian(timeInCycle, p_time, p_sigma);
voltage += q_amp * gaussian(timeInCycle, q_time, qrs_sigma);
voltage += r_amp * gaussian(timeInCycle, r_time, qrs_sigma);
voltage += s_amp * gaussian(timeInCycle, s_time, qrs_sigma);
voltage += t_amp * gaussian(timeInCycle, t_time, t_sigma);
return voltage;
}
std::string Heart::getSummary() const {
std::stringstream ss;
ss.precision(2);
ss << std::fixed;
ss << "--- Heart Summary ---\n"
<< "Heart Rate (Measured): " << getHeartRate() << " bpm\n"
<< "Ejection Fraction: " << getEjectionFraction() * 100.0 << "%\n"
<< "Aortic Pressure: " << getAorticPressure() << " mmHg\n\n"
<< "--- Chambers ---\n"
<< " LV Volume: " << leftVentricle.volume_mL << " mL\n"
<< " LV Pressure: " << leftVentricle.pressure_mmHg << " mmHg\n"
<< " RV Volume: " << rightVentricle.volume_mL << " mL\n"
<< " RV Pressure: " << rightVentricle.pressure_mmHg << " mmHg\n\n"
<< "--- Valves ---\n"
<< " Aortic Valve: " << (aorticValve.status == ValveStatus::OPEN ? "OPEN" : "CLOSED") << "\n"
<< " Mitral Valve: " << (mitralValve.status == ValveStatus::OPEN ? "OPEN" : "CLOSED") << "\n";
return ss.str();
}
double Heart::getHeartRate() const { return measuredHeartRate; }
const std::map<std::string, std::deque<double>>& Heart::getEkgData() const { return ekgData; }
double Heart::getEjectionFraction() const { return ejectionFraction; }
double Heart::getAorticPressure() const {
// Aortic pressure decays over time, but gets "pumped up" by ventricular ejection
// This is a placeholder for a more complex arterial model.
if(aorticValve.status == ValveStatus::OPEN) {
return leftVentricle.pressure_mmHg;
}
// Simplified diastolic pressure decay
return 80.0 + 40.0 * exp(-cardiacCyclePosition_s);
}