351 lines
9.9 KiB
Rust
351 lines
9.9 KiB
Rust
//! MRE solver configuration with non-dimensionalization
|
|
//!
|
|
//! Non-dimensionalization is critical for numerical stability:
|
|
//! - Coordinates scaled by domain size
|
|
//! - Stiffness scaled by reference value (~3 kPa)
|
|
//! - Wave amplitude scaled by typical MRE displacement (~1 um)
|
|
|
|
use mre_shared::TissueProperties;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Non-dimensionalization scales for numerical stability
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
pub struct NonDimScales {
|
|
/// Reference length (domain size) in meters
|
|
pub length_ref: f32,
|
|
/// Reference stiffness in Pascals (not kPa!)
|
|
pub stiffness_ref: f32,
|
|
/// Reference wave amplitude in meters
|
|
pub wave_ref: f32,
|
|
}
|
|
|
|
impl NonDimScales {
|
|
/// Create scales from domain and typical stiffness
|
|
#[must_use]
|
|
pub fn from_domain(domain_size: f32, typical_stiffness_kpa: f32) -> Self {
|
|
Self {
|
|
length_ref: domain_size,
|
|
stiffness_ref: typical_stiffness_kpa * 1000.0, // Convert to Pa
|
|
wave_ref: 1e-6, // 1 micrometer typical MRE displacement
|
|
}
|
|
}
|
|
|
|
/// Non-dimensionalize coordinates: x' = x / L_ref
|
|
#[must_use]
|
|
pub fn nondim_length(&self, x: f32) -> f32 {
|
|
x / self.length_ref
|
|
}
|
|
|
|
/// Dimensionalize coordinates: x = x' * L_ref
|
|
#[must_use]
|
|
pub fn dim_length(&self, x_nondim: f32) -> f32 {
|
|
x_nondim * self.length_ref
|
|
}
|
|
|
|
/// Non-dimensionalize stiffness: mu' = mu / mu_ref
|
|
#[must_use]
|
|
pub fn nondim_stiffness(&self, mu_pa: f32) -> f32 {
|
|
mu_pa / self.stiffness_ref
|
|
}
|
|
|
|
/// Dimensionalize stiffness: mu = mu' * mu_ref (returns Pa)
|
|
#[must_use]
|
|
pub fn dim_stiffness_pa(&self, mu_nondim: f32) -> f32 {
|
|
mu_nondim * self.stiffness_ref
|
|
}
|
|
|
|
/// Dimensionalize stiffness to kPa
|
|
#[must_use]
|
|
pub fn dim_stiffness_kpa(&self, mu_nondim: f32) -> f32 {
|
|
self.dim_stiffness_pa(mu_nondim) / 1000.0
|
|
}
|
|
|
|
/// Non-dimensionalize stiffness from kPa
|
|
#[must_use]
|
|
pub fn nondim_stiffness_kpa(&self, mu_kpa: f32) -> f32 {
|
|
self.nondim_stiffness(mu_kpa * 1000.0)
|
|
}
|
|
|
|
/// Non-dimensionalize wave amplitude
|
|
#[must_use]
|
|
pub fn nondim_wave(&self, u: f32) -> f32 {
|
|
u / self.wave_ref
|
|
}
|
|
|
|
/// Dimensionalize wave amplitude
|
|
#[must_use]
|
|
pub fn dim_wave(&self, u_nondim: f32) -> f32 {
|
|
u_nondim * self.wave_ref
|
|
}
|
|
|
|
/// Get the non-dimensional rho*omega^2 coefficient
|
|
/// In non-dim form: rho' * omega'^2 = (rho * L^2 * omega^2) / mu_ref
|
|
#[must_use]
|
|
pub fn nondim_rho_omega_sq(&self, tissue: &TissueProperties) -> f32 {
|
|
let rho = tissue.density;
|
|
let omega_sq = tissue.omega_squared();
|
|
(rho * self.length_ref * self.length_ref * omega_sq) / self.stiffness_ref
|
|
}
|
|
}
|
|
|
|
impl Default for NonDimScales {
|
|
fn default() -> Self {
|
|
Self::from_domain(0.1, 3.0) // 10cm domain, 3 kPa reference
|
|
}
|
|
}
|
|
|
|
/// Complete MRE solver configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MreConfig {
|
|
// --- Wave Net Architecture ---
|
|
/// Number of hidden layers
|
|
pub wave_net_layers: usize,
|
|
/// Hidden dimension per layer
|
|
pub wave_net_hidden: usize,
|
|
/// Number of Fourier feature frequencies
|
|
pub fourier_features: usize,
|
|
/// Scale for Fourier features (controls frequency range)
|
|
pub fourier_scale: f32,
|
|
|
|
// --- Stiffness Texture ---
|
|
/// Grid resolution for stiffness map (width)
|
|
pub stiffness_nx: usize,
|
|
/// Grid resolution for stiffness map (height)
|
|
pub stiffness_ny: usize,
|
|
/// Minimum stiffness (non-dimensionalized) for stability
|
|
pub min_stiffness: f32,
|
|
|
|
// --- Physics ---
|
|
/// Tissue properties (density, frequency)
|
|
pub tissue: TissueProperties,
|
|
|
|
// --- Non-dimensionalization ---
|
|
/// Scaling factors for numerical stability
|
|
pub nondim: NonDimScales,
|
|
|
|
// --- Training ---
|
|
/// Learning rate for Wave Net parameters
|
|
pub learning_rate_wave: f32,
|
|
/// Learning rate for stiffness texture (typically higher)
|
|
pub learning_rate_stiffness: f32,
|
|
/// Weight for physics loss (PDE residual)
|
|
pub physics_weight: f32,
|
|
/// Weight for data loss (wave field matching)
|
|
pub data_weight: f32,
|
|
/// Weight for Total Variation regularization on stiffness
|
|
pub tv_weight: f32,
|
|
|
|
// --- Collocation ---
|
|
/// Number of collocation points for physics loss
|
|
pub num_collocation: usize,
|
|
/// Number of data points to sample per step
|
|
pub num_data_points: usize,
|
|
|
|
// --- Domain ---
|
|
/// Physical domain width in meters
|
|
pub domain_width: f32,
|
|
/// Physical domain height in meters
|
|
pub domain_height: f32,
|
|
}
|
|
|
|
impl Default for MreConfig {
|
|
fn default() -> Self {
|
|
let domain = 0.1; // 10 cm
|
|
Self {
|
|
// Wave Net
|
|
wave_net_layers: 4,
|
|
wave_net_hidden: 128,
|
|
fourier_features: 32,
|
|
fourier_scale: 10.0,
|
|
|
|
// Stiffness
|
|
stiffness_nx: 64,
|
|
stiffness_ny: 64,
|
|
min_stiffness: 0.1, // 10% of reference
|
|
|
|
// Physics
|
|
tissue: TissueProperties::soft_tissue(),
|
|
|
|
// Non-dim
|
|
nondim: NonDimScales::from_domain(domain, 3.0),
|
|
|
|
// Training
|
|
learning_rate_wave: 1e-4,
|
|
learning_rate_stiffness: 1e-3,
|
|
physics_weight: 1.0,
|
|
data_weight: 10.0,
|
|
tv_weight: 0.01,
|
|
|
|
// Collocation
|
|
num_collocation: 2000,
|
|
num_data_points: 1000,
|
|
|
|
// Domain
|
|
domain_width: domain,
|
|
domain_height: domain,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MreConfig {
|
|
/// Create a new config with default values
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Builder: set wave net depth
|
|
#[must_use]
|
|
pub const fn with_wave_net_layers(mut self, layers: usize) -> Self {
|
|
self.wave_net_layers = layers;
|
|
self
|
|
}
|
|
|
|
/// Builder: set wave net width
|
|
#[must_use]
|
|
pub const fn with_wave_net_hidden(mut self, hidden: usize) -> Self {
|
|
self.wave_net_hidden = hidden;
|
|
self
|
|
}
|
|
|
|
/// Builder: set Fourier features
|
|
#[must_use]
|
|
pub const fn with_fourier_features(mut self, features: usize) -> Self {
|
|
self.fourier_features = features;
|
|
self
|
|
}
|
|
|
|
/// Builder: set stiffness resolution
|
|
#[must_use]
|
|
pub const fn with_stiffness_resolution(mut self, nx: usize, ny: usize) -> Self {
|
|
self.stiffness_nx = nx;
|
|
self.stiffness_ny = ny;
|
|
self
|
|
}
|
|
|
|
/// Builder: set tissue properties
|
|
#[must_use]
|
|
pub const fn with_tissue(mut self, tissue: TissueProperties) -> Self {
|
|
self.tissue = tissue;
|
|
self
|
|
}
|
|
|
|
/// Builder: set learning rates
|
|
#[must_use]
|
|
pub const fn with_learning_rates(mut self, wave_lr: f32, stiffness_lr: f32) -> Self {
|
|
self.learning_rate_wave = wave_lr;
|
|
self.learning_rate_stiffness = stiffness_lr;
|
|
self
|
|
}
|
|
|
|
/// Builder: set loss weights
|
|
#[must_use]
|
|
pub const fn with_loss_weights(mut self, physics: f32, data: f32) -> Self {
|
|
self.physics_weight = physics;
|
|
self.data_weight = data;
|
|
self
|
|
}
|
|
|
|
/// Builder: set domain size
|
|
#[must_use]
|
|
pub fn with_domain(mut self, width: f32, height: f32) -> Self {
|
|
self.domain_width = width;
|
|
self.domain_height = height;
|
|
self.nondim = NonDimScales::from_domain(width.max(height), 3.0);
|
|
self
|
|
}
|
|
|
|
/// Input dimension for Wave Net (2: x, y)
|
|
#[must_use]
|
|
pub const fn input_dim(&self) -> usize {
|
|
2
|
|
}
|
|
|
|
/// Output dimension for Wave Net (2: real, imag)
|
|
#[must_use]
|
|
pub const fn output_dim(&self) -> usize {
|
|
2
|
|
}
|
|
|
|
/// Fourier encoding output dimension (2 * fourier_features for sin + cos)
|
|
#[must_use]
|
|
pub const fn fourier_output_dim(&self) -> usize {
|
|
2 * self.fourier_features
|
|
}
|
|
|
|
/// Get the non-dimensional rho*omega^2 coefficient
|
|
#[must_use]
|
|
pub fn nondim_rho_omega_sq(&self) -> f32 {
|
|
self.nondim.nondim_rho_omega_sq(&self.tissue)
|
|
}
|
|
|
|
/// Convert physical coordinates to non-dimensional
|
|
#[must_use]
|
|
pub fn to_nondim_coords(&self, x: f32, y: f32) -> (f32, f32) {
|
|
(self.nondim.nondim_length(x), self.nondim.nondim_length(y))
|
|
}
|
|
|
|
/// Configuration for high accuracy (slower)
|
|
#[must_use]
|
|
pub fn high_accuracy() -> Self {
|
|
Self::default()
|
|
.with_wave_net_layers(6)
|
|
.with_wave_net_hidden(256)
|
|
.with_fourier_features(64)
|
|
.with_stiffness_resolution(128, 128)
|
|
}
|
|
|
|
/// Configuration for fast inference (demo)
|
|
#[must_use]
|
|
pub fn fast() -> Self {
|
|
Self::default()
|
|
.with_wave_net_layers(3)
|
|
.with_wave_net_hidden(64)
|
|
.with_fourier_features(16)
|
|
.with_stiffness_resolution(32, 32)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_nondim_roundtrip() {
|
|
let scales = NonDimScales::from_domain(0.1, 3.0);
|
|
|
|
// Length roundtrip
|
|
let x = 0.05;
|
|
let x_nd = scales.nondim_length(x);
|
|
let x_back = scales.dim_length(x_nd);
|
|
assert!((x - x_back).abs() < 1e-6);
|
|
|
|
// Stiffness roundtrip
|
|
let mu = 5.0; // kPa
|
|
let mu_nd = scales.nondim_stiffness_kpa(mu);
|
|
let mu_back = scales.dim_stiffness_kpa(mu_nd);
|
|
assert!((mu - mu_back).abs() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_builder() {
|
|
let config = MreConfig::new()
|
|
.with_wave_net_layers(6)
|
|
.with_wave_net_hidden(512)
|
|
.with_stiffness_resolution(128, 128);
|
|
|
|
assert_eq!(config.wave_net_layers, 6);
|
|
assert_eq!(config.wave_net_hidden, 512);
|
|
assert_eq!(config.stiffness_nx, 128);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rho_omega_sq() {
|
|
let config = MreConfig::default();
|
|
let coeff = config.nondim_rho_omega_sq();
|
|
// Should be positive and reasonable magnitude
|
|
assert!(coeff > 0.0);
|
|
assert!(coeff < 1000.0);
|
|
}
|
|
}
|