feat: Implement detailed mechanical heart simulation

This commit introduces a major enhancement to the heart simulation, adding a detailed mechanical model of the cardiac cycle that is synchronized with the existing EKG waveform.

Key features:
- The Heart class is refactored to include data structures for the four chambers (atria and ventricles) and four valves (mitral, tricuspid, aortic, pulmonary).
- The `update` method now simulates the phases of the cardiac cycle, including atrial and ventricular systole and diastole.
- Chamber pressures and volumes are dynamically updated based on the phase of the cycle.
- Valve statuses (open/closed) are determined by pressure gradients between chambers.
- Ejection Fraction is now calculated based on the simulated end-diastolic and end-systolic volumes of the left ventricle.
- The example program has been updated to display a live, detailed summary of the heart's mechanical status, showing pressures, volumes, and valve states as they change over time.
This commit is contained in:
google-labs-jules[bot]
2025-08-20 06:22:24 +00:00
parent cd3a2a5a7e
commit dbe8490cb1
5 changed files with 283 additions and 51 deletions
+170 -19
View File
@@ -2,6 +2,16 @@
#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) {
@@ -11,33 +21,174 @@ static double getFluctuation(double stddev) {
return d(gen);
}
Heart::Heart(int id) : Organ(id, "Heart"), heartRate(75.0), bloodPressureSystolic(120.0), bloodPressureDiastolic(80.0) {}
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(double deltaTime_s) {
const double baseline_hr = 75.0;
const double baseline_bp_systolic = 120.0;
const double baseline_bp_diastolic = 80.0;
const double theta = 0.1; // Mean reversion speed
const double hr_stddev = 0.1;
const double bp_stddev = 0.1;
// --- Electrical Simulation Update ---
totalTime_s += deltaTime_s;
heartRate += getFluctuation(0.01); // Slow variation in underlying rate
heartRate = std::max(60.0, std::min(heartRate, 100.0));
double cycleDuration_s = 60.0 / heartRate;
heartRate += theta * (baseline_hr - heartRate) * deltaTime_s + getFluctuation(hr_stddev * deltaTime_s);
bloodPressureSystolic += theta * (baseline_bp_systolic - bloodPressureSystolic) * deltaTime_s + getFluctuation(bp_stddev * deltaTime_s);
bloodPressureDiastolic += theta * (baseline_bp_diastolic - bloodPressureDiastolic) * deltaTime_s + getFluctuation(bp_stddev * deltaTime_s);
double oldCyclePosition = cardiacCyclePosition_s;
cardiacCyclePosition_s += deltaTime_s;
heartRate = std::clamp(heartRate, 60.0, 100.0);
bloodPressureSystolic = std::clamp(bloodPressureSystolic, 90.0, 120.0);
bloodPressureDiastolic = std::clamp(bloodPressureDiastolic, 60.0, 80.0);
// 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 << "Type: " << organType << " (ID: " << organId << ")\n"
<< " Heart Rate: " << heartRate << " bpm\n"
<< " Blood Pressure: " << bloodPressureSystolic << "/" << bloodPressureDiastolic << " mmHg";
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 heartRate; }
double Heart::getBloodPressureSystolic() const { return bloodPressureSystolic; }
double Heart::getBloodPressureDiastolic() const { return bloodPressureDiastolic; }
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);
}
+14 -3
View File
@@ -15,16 +15,17 @@
#include <memory>
/**
* @brief Initializes a new patient with baseline vital signs and a standard set of organs.
* @brief Initializes a new patient with a specific number of heart leads.
* @param patientId The ID for the new patient.
* @param numHeartLeads The number of EKG leads for the heart.
* @return A Patient struct with default healthy values.
*/
Patient initializePatient(int patientId) {
Patient initializePatient(int patientId, int numHeartLeads) {
Patient patient;
patient.patientId = patientId;
// Initialize default organs
patient.organs.push_back(std::make_unique<Heart>(1));
patient.organs.push_back(std::make_unique<Heart>(1, numHeartLeads));
patient.organs.push_back(std::make_unique<Lungs>(2));
patient.organs.push_back(std::make_unique<Brain>(3));
patient.organs.push_back(std::make_unique<Liver>(4));
@@ -41,6 +42,16 @@ Patient initializePatient(int patientId) {
return patient;
}
/**
* @brief Initializes a new patient with baseline vital signs and a 12-lead heart.
* @param patientId The ID for the new patient.
* @return A Patient struct with default healthy values.
*/
Patient initializePatient(int patientId) {
return initializePatient(patientId, 12);
}
/**
* @brief Updates the patient's state by updating the state of all their organs.
* @param patient The patient to update.