use std::f64::consts::PI;
#[allow(dead_code)]
const C_LIGHT: f64 = 2.99792458e8;
#[allow(dead_code)]
const KB: f64 = 1.380649e-23;
#[allow(dead_code)]
const NA: f64 = 6.02214076e23;
#[derive(Debug, Clone)]
pub struct GrueneisenParameter {
pub beta: f64,
pub c_sound: f64,
pub c_p: f64,
pub rho: f64,
}
impl GrueneisenParameter {
pub fn value(&self) -> f64 {
self.beta * self.c_sound * self.c_sound / self.c_p
}
pub fn water_at_37c() -> Self {
Self {
beta: 4.0e-4,
c_sound: 1540.0,
c_p: 4180.0,
rho: 993.0,
}
}
pub fn soft_tissue() -> Self {
Self {
beta: 4.6e-4,
c_sound: 1540.0,
c_p: 3600.0,
rho: 1040.0,
}
}
pub fn blood() -> Self {
Self {
beta: 6.0e-4,
c_sound: 1580.0,
c_p: 3700.0,
rho: 1060.0,
}
}
}
#[derive(Debug, Clone)]
pub struct PhotoacousticSource {
pub grueneisen: GrueneisenParameter,
pub absorption_coeff_per_m: f64,
pub fluence_j_m2: f64,
}
impl PhotoacousticSource {
pub fn initial_pressure_pa(&self) -> f64 {
self.grueneisen.value() * self.absorption_coeff_per_m * self.fluence_j_m2
}
pub fn stress_confinement_ok(&self, pulse_duration_s: f64, absorber_size_m: f64) -> bool {
let tau_stress = absorber_size_m / self.grueneisen.c_sound;
pulse_duration_s < tau_stress
}
pub fn thermal_confinement_ok(
&self,
pulse_duration_s: f64,
absorber_size_m: f64,
thermal_diffusivity: f64,
) -> bool {
let tau_thermal = (absorber_size_m * absorber_size_m) / (4.0 * thermal_diffusivity);
pulse_duration_s < tau_thermal
}
pub fn far_field_pressure(&self, r_m: f64, t_s: f64, absorber_radius_m: f64) -> f64 {
if r_m <= 0.0 {
return 0.0;
}
let c_s = self.grueneisen.c_sound;
let p0 = self.initial_pressure_pa();
let r_cap = absorber_radius_m;
let t_ret = t_s - r_m / c_s;
let amp = p0 * r_cap / (2.0 * r_m);
let dt_pulse = 2.0 * r_cap / c_s;
let sigma = 0.1 * dt_pulse;
let t_plus = t_ret - r_cap / c_s;
let t_minus = t_ret + r_cap / c_s;
let g = |t_off: f64| -> f64 {
(-0.5 * (t_off / sigma).powi(2)).exp() / (sigma * (2.0 * PI).sqrt())
};
amp * (g(t_plus) - g(t_minus))
}
pub fn frequency_spectrum(
&self,
freq_hz: f64,
absorber_radius_m: f64,
detector_distance_m: f64,
) -> f64 {
if detector_distance_m <= 0.0 {
return 0.0;
}
let p0 = self.initial_pressure_pa();
let c_s = self.grueneisen.c_sound;
let r_cap = absorber_radius_m;
let vol = (4.0 / 3.0) * PI * r_cap.powi(3);
let x = 2.0 * PI * freq_hz * r_cap / c_s;
let sinc_val = if x.abs() < 1.0e-12 { 1.0 } else { x.sin() / x };
p0 * vol * sinc_val.abs() / (4.0 * PI * detector_distance_m)
}
pub fn cutoff_frequency_hz(&self, absorber_radius_m: f64) -> f64 {
self.grueneisen.c_sound / (2.0 * PI * absorber_radius_m)
}
pub fn cylindrical_source_signal(&self, r_m: f64, t_s: f64, cylinder_radius_m: f64) -> f64 {
if r_m <= 0.0 {
return 0.0;
}
let c_s = self.grueneisen.c_sound;
let p0 = self.initial_pressure_pa();
let r_cap = cylinder_radius_m;
let t_arr = r_m / c_s; let t_ret = t_s - t_arr;
let tau_r = r_cap / c_s;
let amp = p0 * r_cap * r_cap / (r_m.sqrt() * c_s * tau_r.max(1.0e-30));
if t_ret.abs() > 2.0 * tau_r {
return 0.0;
}
let phase = PI * t_ret / (2.0 * tau_r);
amp * phase.sin() * (-(t_ret / tau_r).powi(2)).exp()
}
}
#[derive(Debug, Clone)]
pub struct SpectralUnmixing {
pub wavelengths_nm: Vec<f64>,
pub extinction_matrix: Vec<Vec<f64>>,
}
impl SpectralUnmixing {
pub fn unmix(&self, pa_signals: &[f64]) -> Vec<f64> {
let n_lambda = self.wavelengths_nm.len();
let n_chrom = self.extinction_matrix.len();
if n_lambda == 0 || n_chrom == 0 || pa_signals.len() < n_lambda {
return vec![0.0; n_chrom];
}
let mut e = vec![vec![0.0_f64; n_chrom]; n_lambda];
for (j, row) in self.extinction_matrix.iter().enumerate() {
for (i, &val) in row.iter().enumerate().take(n_lambda) {
e[i][j] = val;
}
}
let mut ete = vec![vec![0.0_f64; n_chrom]; n_chrom];
for row in 0..n_chrom {
for col in 0..n_chrom {
let mut sum = 0.0;
for e_row in e.iter().take(n_lambda) {
sum += e_row[row] * e_row[col];
}
ete[row][col] = sum;
}
}
let mut et_pa = vec![0.0_f64; n_chrom];
for row in 0..n_chrom {
let mut sum = 0.0;
for i in 0..n_lambda {
sum += e[i][row] * pa_signals[i];
}
et_pa[row] = sum;
}
gaussian_solve(&ete, &et_pa).unwrap_or_else(|| vec![0.0; n_chrom])
}
pub fn oxygen_saturation(hbo2_conc: f64, hhb_conc: f64) -> f64 {
hbo2_conc / (hbo2_conc + hhb_conc).max(f64::EPSILON)
}
}
fn gaussian_solve(a: &[Vec<f64>], b: &[f64]) -> Option<Vec<f64>> {
let n = b.len();
if a.len() != n {
return None;
}
let mut mat: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row = a[i].clone();
row.push(b[i]);
row
})
.collect();
for col in 0..n {
let mut max_row = col;
let mut max_val = mat[col][col].abs();
for (row, r_data) in mat.iter().enumerate().take(n).skip(col + 1) {
if r_data[col].abs() > max_val {
max_val = r_data[col].abs();
max_row = row;
}
}
if max_val < 1.0e-15 {
return None; }
mat.swap(col, max_row);
let pivot = mat[col][col];
for row in col + 1..n {
let factor = mat[row][col] / pivot;
let pivot_row: Vec<f64> = mat[col][col..=n].to_vec();
for (k_off, &pv) in pivot_row.iter().enumerate() {
mat[row][col + k_off] -= pv * factor;
}
}
}
let mut x = vec![0.0_f64; n];
for i in (0..n).rev() {
let mut sum = mat[i][n];
for j in (i + 1)..n {
sum -= mat[i][j] * x[j];
}
x[i] = sum / mat[i][i];
}
Some(x)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grueneisen_water() {
let g = GrueneisenParameter::water_at_37c();
let gamma = g.value();
assert!(gamma > 0.05 && gamma < 0.5, "Γ={}", gamma);
}
#[test]
fn grueneisen_soft_tissue() {
let g = GrueneisenParameter::soft_tissue();
let gamma = g.value();
assert!(gamma > 0.10 && gamma < 0.40, "Γ_tissue={}", gamma);
}
#[test]
fn initial_pressure_blood() {
let src = PhotoacousticSource {
grueneisen: GrueneisenParameter::soft_tissue(),
absorption_coeff_per_m: 1.0e4,
fluence_j_m2: 0.1,
};
let p0 = src.initial_pressure_pa();
assert!(p0 > 10.0 && p0 < 1000.0, "p0={}Pa", p0);
}
#[test]
fn stress_confinement_nanosecond_pulse() {
let src = PhotoacousticSource {
grueneisen: GrueneisenParameter::soft_tissue(),
absorption_coeff_per_m: 1.0e4,
fluence_j_m2: 0.1,
};
assert!(src.stress_confinement_ok(10.0e-9, 50.0e-6));
assert!(!src.stress_confinement_ok(100.0e-9, 50.0e-6));
}
#[test]
fn cutoff_frequency_sphere() {
let src = PhotoacousticSource {
grueneisen: GrueneisenParameter::soft_tissue(),
absorption_coeff_per_m: 1.0e4,
fluence_j_m2: 0.1,
};
let fc = src.cutoff_frequency_hz(50.0e-6);
assert!(fc > 1.0e6 && fc < 20.0e6, "f_c={}MHz", fc / 1.0e6);
}
#[test]
fn frequency_spectrum_dc_limit() {
let src = PhotoacousticSource {
grueneisen: GrueneisenParameter::soft_tissue(),
absorption_coeff_per_m: 1.0e4,
fluence_j_m2: 0.1,
};
let s = src.frequency_spectrum(1.0, 50.0e-6, 0.01);
assert!(s > 0.0, "DC spectrum must be positive");
}
#[test]
fn spectral_unmixing_two_chromophores() {
let unmix = SpectralUnmixing {
wavelengths_nm: vec![700.0, 800.0],
extinction_matrix: vec![
vec![3.0, 1.0], vec![1.0, 3.0], ],
};
let concs = unmix.unmix(&[5.0, 7.0]);
assert_eq!(concs.len(), 2);
assert!((concs[0] - 1.0).abs() < 1.0e-8, "c0={}", concs[0]);
assert!((concs[1] - 2.0).abs() < 1.0e-8, "c1={}", concs[1]);
}
#[test]
fn oxygen_saturation_full() {
let so2 = SpectralUnmixing::oxygen_saturation(0.98, 0.02);
assert!((so2 - 0.98).abs() < 1.0e-10, "sO2={}", so2);
}
#[test]
fn far_field_pressure_peak_positive() {
let src = PhotoacousticSource {
grueneisen: GrueneisenParameter::soft_tissue(),
absorption_coeff_per_m: 1.0e4,
fluence_j_m2: 0.1,
};
let r = 0.01; let c_s = 1540.0;
let r_abs = 50.0e-6;
let t_peak = r / c_s + r_abs / c_s;
let p = src.far_field_pressure(r, t_peak, r_abs);
assert!(
p > 0.0,
"Leading half of N-pulse should be compressive, got {}",
p
);
}
#[test]
fn c_light_value() {
assert!((C_LIGHT - 2.99792458e8).abs() < 1.0, "C_LIGHT={}", C_LIGHT);
}
}