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
+458
View File
@@ -0,0 +1,458 @@
//! FE Model data structures.
//!
//! This module defines the core data structures for representing
//! finite element models created from MRI data.
use nalgebra::Point3;
use rtx_materials::PronyCoefficients;
use rtx_mesh_gen::TetrahedralMesh;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// A region in the FE model with associated material properties.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelRegion {
/// Region identifier (typically from segmentation label).
pub id: usize,
/// Human-readable name for the region.
pub name: String,
/// Element indices belonging to this region.
pub element_ids: Vec<usize>,
/// Material model for this region.
pub material: RegionMaterial,
}
/// Material assignment for a region.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RegionMaterial {
/// Linear elastic material.
Elastic {
/// Young's modulus (Pa).
young_modulus: f64,
/// Poisson's ratio.
poisson_ratio: f64,
/// Density (kg/m³).
density: f64,
},
/// Viscoelastic material with Prony series.
Viscoelastic {
/// Instantaneous shear modulus (Pa).
g0: f64,
/// Bulk modulus (Pa).
bulk_modulus: f64,
/// Prony series coefficients.
prony_coeffs: PronyCoefficients,
/// Density (kg/m³).
density: f64,
},
/// Spatially varying MRE-derived properties.
MreDerived {
/// Base material (elastic or viscoelastic).
base_material: Box<RegionMaterial>,
/// MRE property values mapped to element nodes.
mre_values: MrePropertyMap,
},
}
/// MRE property mapping to mesh elements.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MrePropertyMap {
/// Storage modulus G' (Pa) at each node.
pub storage_modulus: Vec<f64>,
/// Loss modulus G'' (Pa) at each node.
pub loss_modulus: Vec<f64>,
/// Frequency at which MRE was measured (Hz).
pub frequency: f64,
}
/// Boundary condition definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BoundaryCondition {
/// Fixed displacement (Dirichlet).
Displacement {
/// Node indices with this constraint.
node_ids: Vec<usize>,
/// Fixed displacement vector (None for fixed DOF).
value: Option<[f64; 3]>,
/// Which DOFs are constrained (x, y, z).
constrained: [bool; 3],
},
/// Applied force (Neumann).
Force {
/// Node indices with applied force.
node_ids: Vec<usize>,
/// Force vector (N).
value: [f64; 3],
},
/// Applied pressure on surface.
Pressure {
/// Surface element faces (element_id, face_id).
faces: Vec<(usize, usize)>,
/// Pressure value (Pa).
value: f64,
},
}
/// A complete finite element model for MRI2FE workflow.
#[derive(Debug, Clone)]
pub struct Mri2FeModel {
/// Model name.
pub name: String,
/// Model description.
pub description: String,
/// The tetrahedral mesh.
pub mesh: TetrahedralMesh,
/// Regions with material assignments.
pub regions: Vec<ModelRegion>,
/// Boundary conditions.
pub boundary_conditions: Vec<BoundaryCondition>,
/// Model metadata.
pub metadata: ModelMetadata,
}
/// Model metadata.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ModelMetadata {
/// Source image path.
pub source_image: Option<String>,
/// MRE image paths.
pub mre_images: Vec<String>,
/// Registration transforms applied.
pub transforms: Vec<String>,
/// Creation timestamp.
pub created: Option<String>,
/// Additional properties.
pub properties: HashMap<String, String>,
}
impl Mri2FeModel {
/// Create a new empty FE model.
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
mesh: TetrahedralMesh::new(),
regions: Vec::new(),
boundary_conditions: Vec::new(),
metadata: ModelMetadata::default(),
}
}
/// Get the total number of nodes.
pub fn node_count(&self) -> usize {
self.mesh.vertices.len()
}
/// Get the total number of elements.
pub fn element_count(&self) -> usize {
self.mesh.tetrahedra.len()
}
/// Get a node position by index.
pub fn node(&self, index: usize) -> Option<Point3<f64>> {
self.mesh.vertices.get(index).map(|v| v.position)
}
/// Get an element's node indices.
pub fn element(&self, index: usize) -> Option<[usize; 4]> {
self.mesh.tetrahedra.get(index).map(|t| t.vertices)
}
/// Find the region containing an element.
pub fn region_for_element(&self, element_id: usize) -> Option<&ModelRegion> {
self.regions
.iter()
.find(|r| r.element_ids.contains(&element_id))
}
/// Add a region to the model.
pub fn add_region(&mut self, region: ModelRegion) {
self.regions.push(region);
}
/// Add a boundary condition.
pub fn add_boundary_condition(&mut self, bc: BoundaryCondition) {
self.boundary_conditions.push(bc);
}
/// Validate the model for export.
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if self.mesh.vertices.is_empty() {
errors.push("Mesh has no nodes".to_string());
}
if self.mesh.tetrahedra.is_empty() {
errors.push("Mesh has no elements".to_string());
}
// Check all elements reference valid nodes
let node_count = self.mesh.vertices.len();
for (i, tet) in self.mesh.tetrahedra.iter().enumerate() {
for &node_id in &tet.vertices {
if node_id >= node_count {
errors.push(format!("Element {} references invalid node {}", i, node_id));
}
}
}
// Check region element assignments
let elem_count = self.mesh.tetrahedra.len();
for region in &self.regions {
for &elem_id in &region.element_ids {
if elem_id >= elem_count {
errors.push(format!(
"Region '{}' references invalid element {}",
region.name, elem_id
));
}
}
}
// Check boundary condition node references
for (i, bc) in self.boundary_conditions.iter().enumerate() {
let node_ids = match bc {
BoundaryCondition::Displacement { node_ids, .. } => node_ids,
BoundaryCondition::Force { node_ids, .. } => node_ids,
BoundaryCondition::Pressure { .. } => continue,
};
for &node_id in node_ids {
if node_id >= node_count {
errors.push(format!(
"Boundary condition {} references invalid node {}",
i, node_id
));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
/// Get model bounding box.
pub fn bounding_box(&self) -> Option<(Point3<f64>, Point3<f64>)> {
if self.mesh.vertices.is_empty() {
return None;
}
let points: Vec<Point3<f64>> = self.mesh.vertices.iter().map(|v| v.position).collect();
rtx_segmentation::kdtree::bounding_box(&points)
}
/// Get model center of mass.
pub fn center_of_mass(&self) -> Option<Point3<f64>> {
if self.mesh.vertices.is_empty() {
return None;
}
let points: Vec<Point3<f64>> = self.mesh.vertices.iter().map(|v| v.position).collect();
rtx_segmentation::kdtree::center_of_mass(&points)
}
/// Convert to rtx-fem-export FEModel for export.
pub fn to_fem_model(&self) -> rtx_fem_export::FEModel {
use rtx_fem_export::{ElementType, FEModelBuilder, Material};
use rtx_materials::LinearElastic;
let mut builder = FEModelBuilder::new(&self.name, &self.description);
// Add all nodes
let node_ids: Vec<u64> = self
.mesh
.vertices
.iter()
.map(|v| builder.add_node(v.position.x, v.position.y, v.position.z))
.collect();
// Add materials and parts for each region
for region in &self.regions {
let mat_id = match &region.material {
RegionMaterial::Elastic {
young_modulus,
poisson_ratio,
density,
} => {
let elastic = LinearElastic::new(*young_modulus, *poisson_ratio, *density);
builder.add_material(Material::Elastic(elastic))
}
RegionMaterial::Viscoelastic {
g0: _,
bulk_modulus,
prony_coeffs,
density,
} => {
let km = rtx_materials::KelvinMaxwell::from_prony(
prony_coeffs,
*density,
*bulk_modulus,
);
builder.add_material(Material::KelvinMaxwell(km))
}
RegionMaterial::MreDerived { base_material, .. } => {
// Use base material for export (MRE values are for analysis)
if let RegionMaterial::Elastic {
young_modulus,
poisson_ratio,
density,
} = base_material.as_ref() {
let elastic =
LinearElastic::new(*young_modulus, *poisson_ratio, *density);
builder.add_material(Material::Elastic(elastic))
} else {
let elastic = LinearElastic::new(1e6, 0.45, 1000.0);
builder.add_material(Material::Elastic(elastic))
}
}
};
let part_id = builder.add_part(&region.name, mat_id, ElementType::Tet4);
// Add elements for this region
for &elem_idx in &region.element_ids {
if let Some(tet) = self.mesh.tetrahedra.get(elem_idx) {
builder.add_tet4(
part_id,
node_ids[tet.vertices[0]],
node_ids[tet.vertices[1]],
node_ids[tet.vertices[2]],
node_ids[tet.vertices[3]],
);
}
}
}
// If no regions defined, add all elements to a default part
if self.regions.is_empty() && !self.mesh.tetrahedra.is_empty() {
let elastic = LinearElastic::new(1e6, 0.45, 1000.0);
let mat_id = builder.add_material(Material::Elastic(elastic));
let part_id = builder.add_part("Default", mat_id, ElementType::Tet4);
for tet in &self.mesh.tetrahedra {
builder.add_tet4(
part_id,
node_ids[tet.vertices[0]],
node_ids[tet.vertices[1]],
node_ids[tet.vertices[2]],
node_ids[tet.vertices[3]],
);
}
}
builder.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rtx_mesh_gen::Vertex;
fn create_simple_mesh() -> TetrahedralMesh {
let mut mesh = TetrahedralMesh::new();
mesh.vertices = vec![
Vertex::new(0.0, 0.0, 0.0),
Vertex::new(1.0, 0.0, 0.0),
Vertex::new(0.0, 1.0, 0.0),
Vertex::new(0.0, 0.0, 1.0),
];
mesh.tetrahedra = vec![rtx_mesh_gen::Tetrahedron::new(0, 1, 2, 3)];
mesh
}
#[test]
fn test_new_model() {
let model = Mri2FeModel::new("Test", "A test model");
assert_eq!(model.name, "Test");
assert_eq!(model.description, "A test model");
assert_eq!(model.node_count(), 0);
assert_eq!(model.element_count(), 0);
}
#[test]
fn test_model_with_mesh() {
let mut model = Mri2FeModel::new("Test", "Test model");
model.mesh = create_simple_mesh();
assert_eq!(model.node_count(), 4);
assert_eq!(model.element_count(), 1);
assert!(model.validate().is_ok());
}
#[test]
fn test_region_material() {
let elastic = RegionMaterial::Elastic {
young_modulus: 1e9,
poisson_ratio: 0.3,
density: 1000.0,
};
let region = ModelRegion {
id: 1,
name: "Brain".to_string(),
element_ids: vec![0],
material: elastic,
};
assert_eq!(region.name, "Brain");
assert_eq!(region.element_ids.len(), 1);
}
#[test]
fn test_boundary_conditions() {
let mut model = Mri2FeModel::new("Test", "Test");
model.mesh = create_simple_mesh();
model.add_boundary_condition(BoundaryCondition::Displacement {
node_ids: vec![0],
value: None,
constrained: [true, true, true],
});
model.add_boundary_condition(BoundaryCondition::Force {
node_ids: vec![3],
value: [0.0, 0.0, -100.0],
});
assert_eq!(model.boundary_conditions.len(), 2);
assert!(model.validate().is_ok());
}
#[test]
fn test_invalid_element_reference() {
let mut model = Mri2FeModel::new("Test", "Test");
let mut mesh = TetrahedralMesh::new();
mesh.vertices = vec![Vertex::new(0.0, 0.0, 0.0), Vertex::new(1.0, 0.0, 0.0)];
// Invalid vertex references
mesh.tetrahedra = vec![rtx_mesh_gen::Tetrahedron::new(0, 1, 10, 20)];
model.mesh = mesh;
let result = model.validate();
assert!(result.is_err());
}
#[test]
fn test_to_fem_model() {
let mut model = Mri2FeModel::new("Test", "Test");
model.mesh = create_simple_mesh();
model.regions.push(ModelRegion {
id: 1,
name: "Default".to_string(),
element_ids: vec![0],
material: RegionMaterial::Elastic {
young_modulus: 1e6,
poisson_ratio: 0.3,
density: 1000.0,
},
});
let fem = model.to_fem_model();
assert_eq!(fem.nodes.len(), 4);
assert_eq!(fem.elements.len(), 1);
}
}