Initial commit
This commit is contained in:
@@ -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 ®ion.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 ®ion.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(®ion.name, mat_id, ElementType::Tet4);
|
||||
|
||||
// Add elements for this region
|
||||
for &elem_idx in ®ion.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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user