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
+423
View File
@@ -0,0 +1,423 @@
//! Learnable stiffness texture for MRE inverse problem
//!
//! The stiffness field mu(x,y) is represented as a 2D texture (grid) rather
//! than a neural network. This allows for sharp discontinuities (tumor edges)
//! that neural networks tend to over-smooth.
//!
//! Key features:
//! - Bilinear interpolation for smooth sampling between grid points
//! - Finite difference gradients (mu_x, mu_y) for physics loss
//! - Direct gradient descent on texture pixels
use crate::config::MreConfig;
use anyhow::Result;
use mre_shared::StiffnessField;
use rtx_tensor::{Device, Tensor};
/// Learnable stiffness texture
pub struct StiffnessTexture {
/// Stiffness values [ny, nx] in non-dimensional units
values: Tensor,
/// Grid dimensions
nx: usize,
ny: usize,
/// Physical domain bounds (non-dimensional: [0, 1] x [0, 1])
x_min: f32,
x_max: f32,
y_min: f32,
y_max: f32,
/// Minimum allowed stiffness (non-dimensional)
min_stiffness: f32,
/// Device
device: Device,
}
impl StiffnessTexture {
/// Create a new stiffness texture with uniform initial value
pub fn new(config: &MreConfig, device: &Device) -> Result<Self> {
let nx = config.stiffness_nx;
let ny = config.stiffness_ny;
// Initialize with uniform value = 1.0 (reference stiffness in non-dim)
let init_value = 1.0;
let n = nx * ny;
let data = vec![init_value; n];
let values = Tensor::from_data(data, vec![ny, nx], device)?;
Ok(Self {
values,
nx,
ny,
x_min: 0.0,
x_max: 1.0, // Non-dimensional domain
y_min: 0.0,
y_max: 1.0,
min_stiffness: config.min_stiffness,
device: device.clone(),
})
}
/// Initialize from a ground truth stiffness field
pub fn from_field(field: &StiffnessField, config: &MreConfig, device: &Device) -> Result<Self> {
let (nx, ny) = field.resolution;
// Convert to non-dimensional values
let values_nondim: Vec<f32> = field
.values
.iter()
.map(|&v| config.nondim.nondim_stiffness_kpa(v))
.collect();
let values = Tensor::from_data(values_nondim, vec![ny, nx], device)?;
Ok(Self {
values,
nx,
ny,
x_min: 0.0,
x_max: 1.0,
y_min: 0.0,
y_max: 1.0,
min_stiffness: config.min_stiffness,
device: device.clone(),
})
}
/// Sample stiffness at arbitrary points using bilinear interpolation
///
/// # Arguments
/// * `x` - x-coordinates [batch] (non-dimensional, in [0, 1])
/// * `y` - y-coordinates [batch]
///
/// # Returns
/// Tensor [batch] with interpolated stiffness values
pub fn sample(&self, x: &Tensor, y: &Tensor) -> Result<Tensor> {
let batch_size = x.shape().dims()[0];
let x_data = x.to_cpu()?;
let y_data = y.to_cpu()?;
let values_data = self.values.to_cpu()?;
let mut result = Vec::with_capacity(batch_size);
for i in 0..batch_size {
let xi = x_data[i];
let yi = y_data[i];
let mu = self.bilinear_interpolate(&values_data, xi, yi);
result.push(mu.max(self.min_stiffness));
}
Ok(Tensor::from_data(result, vec![batch_size], &self.device)?)
}
/// Sample stiffness and its spatial gradients
///
/// Uses finite differences on the texture for gradient computation.
///
/// # Returns
/// (mu, mu_x, mu_y) - stiffness and its gradients
pub fn sample_with_gradients(
&self,
x: &Tensor,
y: &Tensor,
) -> Result<(Tensor, Tensor, Tensor)> {
let batch_size = x.shape().dims()[0];
let x_data = x.to_cpu()?;
let y_data = y.to_cpu()?;
let values_data = self.values.to_cpu()?;
let dx = (self.x_max - self.x_min) / (self.nx - 1).max(1) as f32;
let dy = (self.y_max - self.y_min) / (self.ny - 1).max(1) as f32;
let mut mu_result = Vec::with_capacity(batch_size);
let mut mu_x_result = Vec::with_capacity(batch_size);
let mut mu_y_result = Vec::with_capacity(batch_size);
for i in 0..batch_size {
let xi = x_data[i];
let yi = y_data[i];
// Sample mu at current point
let mu = self.bilinear_interpolate(&values_data, xi, yi);
// Central difference for gradients
let mu_xp = self.bilinear_interpolate(&values_data, xi + dx, yi);
let mu_xm = self.bilinear_interpolate(&values_data, xi - dx, yi);
let mu_yp = self.bilinear_interpolate(&values_data, xi, yi + dy);
let mu_ym = self.bilinear_interpolate(&values_data, xi, yi - dy);
let mu_x = (mu_xp - mu_xm) / (2.0 * dx);
let mu_y = (mu_yp - mu_ym) / (2.0 * dy);
mu_result.push(mu.max(self.min_stiffness));
mu_x_result.push(mu_x);
mu_y_result.push(mu_y);
}
let mu = Tensor::from_data(mu_result, vec![batch_size], &self.device)?;
let mu_x = Tensor::from_data(mu_x_result, vec![batch_size], &self.device)?;
let mu_y = Tensor::from_data(mu_y_result, vec![batch_size], &self.device)?;
Ok((mu, mu_x, mu_y))
}
/// Bilinear interpolation helper
fn bilinear_interpolate(&self, data: &[f32], x: f32, y: f32) -> f32 {
// Convert to texture coordinates
let tx = (x - self.x_min) / (self.x_max - self.x_min);
let ty = (y - self.y_min) / (self.y_max - self.y_min);
// Scale to grid indices
let fx = tx * (self.nx - 1) as f32;
let fy = ty * (self.ny - 1) as f32;
// Floor indices
let ix0 = (fx.floor() as usize).min(self.nx - 1);
let iy0 = (fy.floor() as usize).min(self.ny - 1);
let ix1 = (ix0 + 1).min(self.nx - 1);
let iy1 = (iy0 + 1).min(self.ny - 1);
// Fractional parts (weights)
let wx1 = fx - ix0 as f32;
let wy1 = fy - iy0 as f32;
let wx0 = 1.0 - wx1;
let wy0 = 1.0 - wy1;
// Gather values at four corners
let v00 = data[iy0 * self.nx + ix0];
let v01 = data[iy0 * self.nx + ix1];
let v10 = data[iy1 * self.nx + ix0];
let v11 = data[iy1 * self.nx + ix1];
// Bilinear interpolation
v00 * wx0 * wy0 + v01 * wx1 * wy0 + v10 * wx0 * wy1 + v11 * wx1 * wy1
}
/// Apply gradient update to texture values
///
/// # Arguments
/// * `grad` - Gradient tensor [ny, nx]
/// * `lr` - Learning rate
pub fn apply_gradient(&mut self, grad: &Tensor, lr: f32) -> Result<()> {
// values = values - lr * grad
let update = grad.mul_scalar(lr)?;
self.values = self.values.sub(&update)?;
// Clamp to minimum stiffness
self.clamp_values()?;
Ok(())
}
/// Compute gradient of stiffness texture from physics residual
///
/// Given the physics residual R and its gradient dR/dmu, compute
/// the gradient dL/d(texture) by scattering residual contributions
/// back to texture pixels.
///
/// # Arguments
/// * `x` - sample x-coordinates [batch]
/// * `y` - sample y-coordinates [batch]
/// * `grad_mu` - gradient dL/dmu at each sample point [batch]
pub fn compute_texture_gradient(
&self,
x: &Tensor,
y: &Tensor,
grad_mu: &Tensor,
) -> Result<Tensor> {
let batch_size = x.shape().dims()[0];
let x_data = x.to_cpu()?;
let y_data = y.to_cpu()?;
let grad_data = grad_mu.to_cpu()?;
// Accumulate gradients using bilinear splatting (inverse of interpolation)
let mut texture_grad = vec![0.0f32; self.ny * self.nx];
for i in 0..batch_size {
let xi = x_data[i];
let yi = y_data[i];
let grad = grad_data[i];
// Convert to texture coordinates
let tx = (xi - self.x_min) / (self.x_max - self.x_min);
let ty = (yi - self.y_min) / (self.y_max - self.y_min);
let fx = tx * (self.nx - 1) as f32;
let fy = ty * (self.ny - 1) as f32;
let ix0 = (fx.floor() as usize).min(self.nx - 1);
let iy0 = (fy.floor() as usize).min(self.ny - 1);
let ix1 = (ix0 + 1).min(self.nx - 1);
let iy1 = (iy0 + 1).min(self.ny - 1);
let wx1 = fx - ix0 as f32;
let wy1 = fy - iy0 as f32;
let wx0 = 1.0 - wx1;
let wy0 = 1.0 - wy1;
// Splat gradient to four corners (inverse bilinear)
texture_grad[iy0 * self.nx + ix0] += grad * wx0 * wy0;
texture_grad[iy0 * self.nx + ix1] += grad * wx1 * wy0;
texture_grad[iy1 * self.nx + ix0] += grad * wx0 * wy1;
texture_grad[iy1 * self.nx + ix1] += grad * wx1 * wy1;
}
Ok(Tensor::from_data(
texture_grad,
vec![self.ny, self.nx],
&self.device,
)?)
}
/// Clamp values to minimum stiffness
fn clamp_values(&mut self) -> Result<()> {
let data = self.values.to_cpu()?;
let clamped: Vec<f32> = data.iter().map(|&v| v.max(self.min_stiffness)).collect();
self.values = Tensor::from_data(clamped, vec![self.ny, self.nx], &self.device)?;
Ok(())
}
/// Convert to StiffnessField for visualization
pub fn to_field(&self, config: &MreConfig) -> Result<StiffnessField> {
let data = self.values.to_cpu()?;
// Convert from non-dim to kPa
let values_kpa: Vec<f32> = data
.iter()
.map(|&v| config.nondim.dim_stiffness_kpa(v))
.collect();
Ok(StiffnessField {
resolution: (self.nx, self.ny),
values: values_kpa,
bounds: (
config.nondim.dim_length(self.x_min),
config.nondim.dim_length(self.x_max),
config.nondim.dim_length(self.y_min),
config.nondim.dim_length(self.y_max),
),
})
}
/// Get grid dimensions
pub fn resolution(&self) -> (usize, usize) {
(self.nx, self.ny)
}
/// Get raw values tensor
pub fn values(&self) -> &Tensor {
&self.values
}
/// Compute Total Variation (TV) regularization loss
///
/// TV = sum(|mu(i+1,j) - mu(i,j)| + |mu(i,j+1) - mu(i,j)|)
///
/// Encourages piecewise-constant stiffness maps (sharp edges)
pub fn tv_loss(&self) -> Result<f32> {
let data = self.values.to_cpu()?;
let mut tv = 0.0f32;
for j in 0..self.ny {
for i in 0..self.nx {
let idx = j * self.nx + i;
let v = data[idx];
// Horizontal difference
if i + 1 < self.nx {
let v_right = data[idx + 1];
tv += (v_right - v).abs();
}
// Vertical difference
if j + 1 < self.ny {
let v_below = data[idx + self.nx];
tv += (v_below - v).abs();
}
}
}
Ok(tv)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_test_config() -> MreConfig {
MreConfig::fast()
}
#[test]
fn test_texture_creation() {
let config = get_test_config();
let device = Device::try_default().unwrap();
let texture = StiffnessTexture::new(&config, &device).unwrap();
assert_eq!(
texture.resolution(),
(config.stiffness_nx, config.stiffness_ny)
);
}
#[test]
fn test_uniform_sampling() {
let config = get_test_config();
let device = Device::try_default().unwrap();
let texture = StiffnessTexture::new(&config, &device).unwrap();
let x = Tensor::from_data(vec![0.25, 0.5, 0.75], vec![3], &device).unwrap();
let y = Tensor::from_data(vec![0.25, 0.5, 0.75], vec![3], &device).unwrap();
let mu = texture.sample(&x, &y).unwrap();
let mu_data = mu.to_cpu().unwrap();
// All should be 1.0 (initial uniform value)
for val in &mu_data {
assert!((*val - 1.0).abs() < 1e-5);
}
}
#[test]
fn test_bilinear_interpolation() {
let config = MreConfig::fast().with_stiffness_resolution(3, 3);
let device = Device::try_default().unwrap();
// Create texture with known pattern
let mut texture = StiffnessTexture::new(&config, &device).unwrap();
// Set corners to different values
// [0,0]=1, [0,2]=2, [2,0]=3, [2,2]=4
let values = vec![1.0, 1.5, 2.0, 2.0, 2.5, 3.0, 3.0, 3.5, 4.0];
texture.values = Tensor::from_data(values, vec![3, 3], &device).unwrap();
// Sample at center (0.5, 0.5) should interpolate
let x = Tensor::from_data(vec![0.5], vec![1], &device).unwrap();
let y = Tensor::from_data(vec![0.5], vec![1], &device).unwrap();
let mu = texture.sample(&x, &y).unwrap();
let mu_val = mu.to_cpu().unwrap()[0];
// Center value should be 2.5
assert!((mu_val - 2.5).abs() < 1e-5);
}
#[test]
fn test_gradient_computation() {
let config = get_test_config();
let device = Device::try_default().unwrap();
let texture = StiffnessTexture::new(&config, &device).unwrap();
let x = Tensor::from_data(vec![0.5], vec![1], &device).unwrap();
let y = Tensor::from_data(vec![0.5], vec![1], &device).unwrap();
let (mu, mu_x, mu_y) = texture.sample_with_gradients(&x, &y).unwrap();
// For uniform texture, gradients should be ~0
let mu_x_val = mu_x.to_cpu().unwrap()[0];
let mu_y_val = mu_y.to_cpu().unwrap()[0];
assert!(mu_x_val.abs() < 1e-5);
assert!(mu_y_val.abs() < 1e-5);
}
}