Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
+275
View File
@@ -0,0 +1,275 @@
//! Pennes Bioheat Equation residual computation
//!
//! The Pennes equation models heat transfer in biological tissue:
//! ```text
//! ρc(∂T/∂t) = k∇²T + ωb·ρb·cb·(Ta - T) + Qm + Qs
//! ```
//!
//! Rearranged as residual (should equal zero):
//! ```text
//! R = ρc·∂T/∂t - k·∇²T - ωb·ρb·cb·(Ta - T) - Qm - Qs = 0
//! ```
use bioheat_shared::BioheatParams;
use serde::{Deserialize, Serialize};
/// Pennes bioheat equation residual computer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PennesResidual {
/// Tissue and blood properties
pub params: BioheatParams,
}
/// Derivatives of temperature field needed for residual computation
#[derive(Debug, Clone, Copy)]
pub struct TemperatureDerivatives {
/// Temperature value T
pub t: f32,
/// Time derivative ∂T/∂t
pub dt_dt: f32,
/// First spatial derivatives
pub dt_dx: f32,
pub dt_dy: f32,
pub dt_dz: f32,
/// Second spatial derivatives (for Laplacian)
pub d2t_dx2: f32,
pub d2t_dy2: f32,
pub d2t_dz2: f32,
}
impl TemperatureDerivatives {
/// Compute the Laplacian ∇²T = ∂²T/∂x² + ∂²T/∂y² + ∂²T/∂z²
#[must_use]
pub fn laplacian(&self) -> f32 {
self.d2t_dx2 + self.d2t_dy2 + self.d2t_dz2
}
/// Create derivatives for a constant temperature field
#[must_use]
pub fn constant(t: f32) -> Self {
Self {
t,
dt_dt: 0.0,
dt_dx: 0.0,
dt_dy: 0.0,
dt_dz: 0.0,
d2t_dx2: 0.0,
d2t_dy2: 0.0,
d2t_dz2: 0.0,
}
}
}
impl PennesResidual {
/// Create a new Pennes residual computer with given parameters
#[must_use]
pub fn new(params: BioheatParams) -> Self {
Self { params }
}
/// Compute the Pennes equation residual at a single point
///
/// # Arguments
/// * `derivs` - Temperature and its derivatives at the point
/// * `heat_source` - External heat source Qs (W/m³) from probe
///
/// # Returns
/// The residual value (should be zero for a correct solution)
#[must_use]
pub fn compute(&self, derivs: &TemperatureDerivatives, heat_source: f32) -> f32 {
let tissue = &self.params.tissue;
let blood = &self.params.blood;
// Volumetric heat capacity ρc (J/m³/K)
let rho_c = tissue.density * tissue.specific_heat;
// Thermal conductivity k (W/m/K)
let k = tissue.thermal_conductivity;
// Perfusion coefficient ωb·ρb·cb (W/m³/K)
let perfusion_coeff = self.params.perfusion_coefficient();
// Metabolic heat Qm (W/m³)
let q_m = tissue.metabolic_heat;
// Arterial temperature Ta (°C)
let t_a = blood.arterial_temperature;
// Compute each term:
// 1. Time derivative term: ρc·∂T/∂t
let time_term = rho_c * derivs.dt_dt;
// 2. Diffusion term: k·∇²T
let diffusion_term = k * derivs.laplacian();
// 3. Perfusion term: ωb·ρb·cb·(Ta - T)
// Note: This is a cooling term when T > Ta
let perfusion_term = perfusion_coeff * (t_a - derivs.t);
// 4. Total source: Qm + Qs
let source_term = q_m + heat_source;
// Residual: ρc·∂T/∂t - k·∇²T - perfusion - Qm - Qs = 0
time_term - diffusion_term - perfusion_term - source_term
}
/// Compute residual for steady-state (∂T/∂t = 0)
#[must_use]
pub fn compute_steady_state(&self, derivs: &TemperatureDerivatives, heat_source: f32) -> f32 {
let mut steady_derivs = *derivs;
steady_derivs.dt_dt = 0.0;
self.compute(&steady_derivs, heat_source)
}
/// Compute residual for batch of points
pub fn compute_batch(
&self,
derivs_batch: &[TemperatureDerivatives],
heat_sources: &[f32],
) -> Vec<f32> {
derivs_batch
.iter()
.zip(heat_sources)
.map(|(d, &q)| self.compute(d, q))
.collect()
}
/// Get the characteristic time scale (thermal diffusion time)
/// τ = L² / α where α = k/(ρc) is thermal diffusivity
#[must_use]
pub fn thermal_time_scale(&self, length_scale: f32) -> f32 {
let alpha = self.params.tissue.thermal_diffusivity();
length_scale * length_scale / alpha
}
/// Get the perfusion time scale
/// τ_p = (ρc) / (ωb·ρb·cb)
#[must_use]
pub fn perfusion_time_scale(&self) -> f32 {
let rho_c = self.params.tissue.volumetric_heat_capacity();
let perf_coeff = self.params.perfusion_coefficient();
if perf_coeff > 1e-10 {
rho_c / perf_coeff
} else {
f32::INFINITY // No perfusion
}
}
}
/// Analytical test case: 1D heat conduction with constant source
/// Useful for validating the residual computation
pub mod analytical {
/// 1D steady-state solution with constant heat source
/// For boundary conditions T(0) = T(L) = T_boundary
/// and constant source Q, the solution is:
/// T(x) = T_boundary + (Q/2k) * x * (L - x)
pub struct SteadyStateBar {
pub length: f32,
pub t_boundary: f32,
pub heat_source: f32,
pub k: f32,
}
impl SteadyStateBar {
/// Temperature at position x
#[must_use]
pub fn temperature(&self, x: f32) -> f32 {
let q_over_2k = self.heat_source / (2.0 * self.k);
self.t_boundary + q_over_2k * x * (self.length - x)
}
/// First derivative dT/dx
#[must_use]
pub fn dt_dx(&self, x: f32) -> f32 {
let q_over_2k = self.heat_source / (2.0 * self.k);
q_over_2k * (self.length - 2.0 * x)
}
/// Second derivative d²T/dx²
#[must_use]
pub fn d2t_dx2(&self) -> f32 {
-self.heat_source / self.k
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pennes_residual_equilibrium() {
// At equilibrium with no heat source and T = Ta, residual should be ~0
let params = BioheatParams::liver_ablation();
let residual = PennesResidual::new(params.clone());
let derivs = TemperatureDerivatives {
t: params.blood.arterial_temperature, // T = Ta
dt_dt: 0.0, // Steady state
dt_dx: 0.0,
dt_dy: 0.0,
dt_dz: 0.0,
d2t_dx2: 0.0,
d2t_dy2: 0.0,
d2t_dz2: 0.0,
};
// Only metabolic heat contributes
let r = residual.compute(&derivs, 0.0);
assert!((r + params.tissue.metabolic_heat).abs() < 1.0);
}
#[test]
fn test_laplacian() {
let derivs = TemperatureDerivatives {
t: 50.0,
dt_dt: 0.0,
dt_dx: 1.0,
dt_dy: 2.0,
dt_dz: 3.0,
d2t_dx2: 1.0,
d2t_dy2: 2.0,
d2t_dz2: 3.0,
};
assert!((derivs.laplacian() - 6.0).abs() < 1e-6);
}
#[test]
fn test_analytical_bar() {
use analytical::SteadyStateBar;
let bar = SteadyStateBar {
length: 1.0,
t_boundary: 37.0,
heat_source: 1000.0,
k: 0.5,
};
// At x=0 and x=L, T should equal boundary
assert!((bar.temperature(0.0) - 37.0).abs() < 1e-6);
assert!((bar.temperature(1.0) - 37.0).abs() < 1e-6);
// Maximum at center
let t_center = bar.temperature(0.5);
assert!(t_center > 37.0);
// Derivative at center should be 0
assert!(bar.dt_dx(0.5).abs() < 1e-6);
}
#[test]
fn test_time_scales() {
let params = BioheatParams::liver_ablation();
let residual = PennesResidual::new(params);
// Thermal time scale for 1cm domain
let tau_thermal = residual.thermal_time_scale(0.01);
assert!(tau_thermal > 0.0);
// Perfusion time scale
let tau_perf = residual.perfusion_time_scale();
assert!(tau_perf > 0.0);
assert!(tau_perf < f32::INFINITY);
}
}