// Copyright (c) 2024 RustyTorch++ Team // Licensed under the Apache License, Version 2.0 //! Isoparametric element utilities and coordinate transformations. use super::{FiniteElement, NaturalCoords}; use crate::error::{ElementError, FeaResult}; use nalgebra::Vector3; /// Isoparametric mapping utilities for finite elements. pub struct IsoparametricMapping; impl IsoparametricMapping { /// Check if an element is isoparametric (same shape functions for geometry and field interpolation). pub fn is_isoparametric(_element: &dyn FiniteElement) -> bool { // All standard finite elements are isoparametric // This could be extended to check specific conditions true } /// Compute the mapping quality at a point. pub fn mapping_quality( element: &dyn FiniteElement, coords: &NaturalCoords, node_coords: &[Vector3], ) -> FeaResult { let jacobian_eval = element.jacobian(coords, node_coords)?; let determinant = jacobian_eval.determinant(); let condition_number = jacobian_eval.condition_number(); let is_valid = jacobian_eval.is_valid(); // Compute distortion measures let aspect_ratio = Self::compute_aspect_ratio(&jacobian_eval); let skewness = Self::compute_skewness(&jacobian_eval); let orthogonality = Self::compute_orthogonality(&jacobian_eval); Ok(MappingQuality { determinant, condition_number, aspect_ratio, skewness, orthogonality, is_valid, }) } /// Analyze mapping quality over the entire element. pub fn analyze_element_mapping( element: &dyn FiniteElement, node_coords: &[Vector3], num_sample_points: usize, ) -> FeaResult { let sample_points = Self::generate_sample_points(element, num_sample_points)?; let mut qualities = Vec::new(); for point in &sample_points { let quality = Self::mapping_quality(element, point, node_coords)?; qualities.push(quality); } let min_determinant = qualities .iter() .map(|q| q.determinant) .fold(f64::INFINITY, f64::min); let max_determinant = qualities .iter() .map(|q| q.determinant) .fold(f64::NEG_INFINITY, f64::max); let avg_determinant = qualities.iter().map(|q| q.determinant).sum::() / qualities.len() as f64; let min_condition = qualities .iter() .map(|q| q.condition_number) .fold(f64::INFINITY, f64::min); let max_condition = qualities .iter() .map(|q| q.condition_number) .fold(f64::NEG_INFINITY, f64::max); let avg_condition = qualities.iter().map(|q| q.condition_number).sum::() / qualities.len() as f64; let max_aspect_ratio = qualities .iter() .map(|q| q.aspect_ratio) .fold(f64::NEG_INFINITY, f64::max); let max_skewness = qualities .iter() .map(|q| q.skewness) .fold(f64::NEG_INFINITY, f64::max); let min_orthogonality = qualities .iter() .map(|q| q.orthogonality) .fold(f64::INFINITY, f64::min); let invalid_count = qualities.iter().filter(|q| !q.is_valid).count(); Ok(ElementMappingAnalysis { sample_points: sample_points.len(), min_determinant, max_determinant, avg_determinant, min_condition, max_condition, avg_condition, max_aspect_ratio, max_skewness, min_orthogonality, invalid_count, is_acceptable: Self::is_mapping_acceptable(&qualities), }) } /// Generate sample points for mapping analysis. fn generate_sample_points( element: &dyn FiniteElement, num_points: usize, ) -> FeaResult> { let param_dim = element.parametric_dimension(); let mut points = Vec::new(); match param_dim { 1 => { // 1D: distribute points along xi for i in 0..num_points { let xi = -1.0 + 2.0 * i as f64 / (num_points - 1) as f64; points.push(NaturalCoords::new_1d(xi)); } } 2 => { // 2D: grid of points let points_per_dim = (num_points as f64).sqrt().ceil() as usize; for i in 0..points_per_dim { for j in 0..points_per_dim { match element.element_type().topology_family() { crate::mesh::ElementTopology::Triangle => { let xi = i as f64 / points_per_dim as f64; let eta = j as f64 / points_per_dim as f64; if xi + eta <= 1.0 { points.push(NaturalCoords::new_2d(xi, eta)); } } crate::mesh::ElementTopology::Quadrilateral => { let xi = -1.0 + 2.0 * i as f64 / (points_per_dim - 1) as f64; let eta = -1.0 + 2.0 * j as f64 / (points_per_dim - 1) as f64; points.push(NaturalCoords::new_2d(xi, eta)); } _ => { return Err(ElementError::UnsupportedElementType { element_type: format!("{:?}", element.element_type()), } .into()); } } } } } 3 => { // 3D: cube or tetrahedral sampling let points_per_dim = (num_points as f64).cbrt().ceil() as usize; for i in 0..points_per_dim { for j in 0..points_per_dim { for k in 0..points_per_dim { match element.element_type().topology_family() { crate::mesh::ElementTopology::Tetrahedron => { let xi = i as f64 / points_per_dim as f64; let eta = j as f64 / points_per_dim as f64; let zeta = k as f64 / points_per_dim as f64; if xi + eta + zeta <= 1.0 { points.push(NaturalCoords::new_3d(xi, eta, zeta)); } } crate::mesh::ElementTopology::Hexahedron => { let xi = -1.0 + 2.0 * i as f64 / (points_per_dim - 1) as f64; let eta = -1.0 + 2.0 * j as f64 / (points_per_dim - 1) as f64; let zeta = -1.0 + 2.0 * k as f64 / (points_per_dim - 1) as f64; points.push(NaturalCoords::new_3d(xi, eta, zeta)); } _ => { return Err(ElementError::UnsupportedElementType { element_type: format!("{:?}", element.element_type()), } .into()); } } } } } } _ => { return Err(ElementError::InvalidGeometry { message: format!("Unsupported parametric dimension: {param_dim}"), } .into()); } } if points.is_empty() { points.push(NaturalCoords::new_3d(0.0, 0.0, 0.0)); // Center point } Ok(points) } /// Compute aspect ratio from Jacobian. fn compute_aspect_ratio(jacobian_eval: &super::JacobianEval) -> f64 { let j = &jacobian_eval.jacobian; let (rows, cols) = (j.nrows(), j.ncols()); match (rows, cols) { (2, 2) => { // 2D: ratio of maximum to minimum singular values let j11 = j[(0, 0)]; let j12 = j[(0, 1)]; let j21 = j[(1, 0)]; let j22 = j[(1, 1)]; let a = j11 * j11 + j21 * j21; let b = j12 * j12 + j22 * j22; let c = j11 * j12 + j21 * j22; let trace = a + b; let det = a * b - c * c; if det <= 0.0 { return f64::INFINITY; } let sqrt_discriminant = ((trace * trace - 4.0 * det).max(0.0)).sqrt(); let sigma_max = f64::midpoint(trace, sqrt_discriminant).sqrt(); let sigma_min = ((trace - sqrt_discriminant) / 2.0).sqrt(); if sigma_min > 1e-12 { sigma_max / sigma_min } else { f64::INFINITY } } (3, 3) => { // 3D: proper aspect ratio using singular value decomposition // Compute column norms of Jacobian matrix (equivalent to principal stretches) let mut max_length: f64 = 0.0; let mut min_length: f64 = f64::INFINITY; for i in 0..3 { let length = (0..3) .map(|row| j[(row, i)] * j[(row, i)]) .sum::() .sqrt(); max_length = max_length.max(length); min_length = min_length.min(length); } if min_length > 1e-12 { max_length / min_length } else { f64::INFINITY } } _ => 1.0, // Default for unsupported dimensions } } /// Compute skewness from Jacobian. fn compute_skewness(jacobian_eval: &super::JacobianEval) -> f64 { let j = &jacobian_eval.jacobian; let (rows, cols) = (j.nrows(), j.ncols()); match (rows, cols) { (2, 2) => { // 2D: angle between coordinate lines let v1 = Vector3::new(j[(0, 0)], j[(1, 0)], 0.0); let v2 = Vector3::new(j[(0, 1)], j[(1, 1)], 0.0); let dot = v1.dot(&v2); let norm1 = v1.norm(); let norm2 = v2.norm(); if norm1 > 1e-12 && norm2 > 1e-12 { let cos_angle = (dot / (norm1 * norm2)).clamp(-1.0, 1.0); let angle = cos_angle.acos(); (angle - std::f64::consts::PI / 2.0).abs() / (std::f64::consts::PI / 2.0) } else { 1.0 } } (3, 3) => { // 3D: maximum skewness among all coordinate plane pairs let mut max_skewness: f64 = 0.0; for i in 0..3 { for k in (i + 1)..3 { let v1 = Vector3::new(j[(0, i)], j[(1, i)], j[(2, i)]); let v2 = Vector3::new(j[(0, k)], j[(1, k)], j[(2, k)]); let dot = v1.dot(&v2); let norm1 = v1.norm(); let norm2 = v2.norm(); if norm1 > 1e-12 && norm2 > 1e-12 { let cos_angle = (dot / (norm1 * norm2)).clamp(-1.0, 1.0); let angle = cos_angle.acos(); let skewness = (angle - std::f64::consts::PI / 2.0).abs() / (std::f64::consts::PI / 2.0); max_skewness = max_skewness.max(skewness); } } } max_skewness } _ => 0.0, } } /// Compute orthogonality measure. fn compute_orthogonality(jacobian_eval: &super::JacobianEval) -> f64 { 1.0 - Self::compute_skewness(jacobian_eval) } /// Check if mapping is acceptable based on quality metrics. fn is_mapping_acceptable(qualities: &[MappingQuality]) -> bool { qualities.iter().all(|q| { q.is_valid && q.determinant > 1e-12 && q.condition_number < 1000.0 && q.aspect_ratio < 100.0 && q.skewness < 0.8 }) } } /// Mapping quality metrics at a point. #[derive(Debug, Clone)] pub struct MappingQuality { /// Jacobian determinant pub determinant: f64, /// Condition number pub condition_number: f64, /// Aspect ratio pub aspect_ratio: f64, /// Skewness measure [0, 1] pub skewness: f64, /// Orthogonality measure [0, 1] pub orthogonality: f64, /// Whether the mapping is valid pub is_valid: bool, } impl MappingQuality { /// Check if this mapping quality is acceptable. pub fn is_acceptable(&self) -> bool { self.is_valid && self.determinant > 1e-12 && self.condition_number < 1000.0 && self.aspect_ratio < 100.0 && self.skewness < 0.8 } /// Get an overall quality score [0, 1] where 1 is perfect. pub fn quality_score(&self) -> f64 { if !self.is_valid { return 0.0; } let det_score = if self.determinant > 1e-12 { 1.0 } else { 0.0 }; let condition_score = (1000.0 / self.condition_number.max(1.0)).min(1.0); let aspect_score = (10.0 / self.aspect_ratio.max(1.0)).min(1.0); let skew_score = 1.0 - self.skewness; (det_score + condition_score + aspect_score + skew_score) / 4.0 } } /// Element mapping analysis results. #[derive(Debug, Clone)] pub struct ElementMappingAnalysis { pub sample_points: usize, pub min_determinant: f64, pub max_determinant: f64, pub avg_determinant: f64, pub min_condition: f64, pub max_condition: f64, pub avg_condition: f64, pub max_aspect_ratio: f64, pub max_skewness: f64, pub min_orthogonality: f64, pub invalid_count: usize, pub is_acceptable: bool, } impl std::fmt::Display for ElementMappingAnalysis { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!( f, "Element Mapping Analysis ({} sample points):", self.sample_points )?; writeln!( f, " Jacobian determinant: {:.6e} - {:.6e} (avg: {:.6e})", self.min_determinant, self.max_determinant, self.avg_determinant )?; writeln!( f, " Condition number: {:.2} - {:.2} (avg: {:.2})", self.min_condition, self.max_condition, self.avg_condition )?; writeln!(f, " Max aspect ratio: {:.2}", self.max_aspect_ratio)?; writeln!(f, " Max skewness: {:.3}", self.max_skewness)?; writeln!(f, " Min orthogonality: {:.3}", self.min_orthogonality)?; writeln!(f, " Invalid points: {}", self.invalid_count)?; writeln!( f, " Overall assessment: {}", if self.is_acceptable { "Acceptable" } else { "Poor" } )?; Ok(()) } } /// Coordinate transformation utilities. pub struct CoordinateTransform; impl CoordinateTransform { /// Transform a vector from natural to physical coordinates. pub fn transform_vector( element: &dyn FiniteElement, coords: &NaturalCoords, node_coords: &[Vector3], natural_vector: &Vector3, ) -> FeaResult> { let jacobian_eval = element.jacobian(coords, node_coords)?; let spatial_dim = element.spatial_dimension(); let param_dim = element.parametric_dimension(); match (spatial_dim, param_dim) { (2, 2) => { let j = &jacobian_eval.jacobian; let physical_vector = Vector3::new( j[(0, 0)] * natural_vector.x + j[(0, 1)] * natural_vector.y, j[(1, 0)] * natural_vector.x + j[(1, 1)] * natural_vector.y, 0.0, ); Ok(physical_vector) } (3, 3) => { let j = &jacobian_eval.jacobian; let physical_vector = Vector3::new( j[(0, 0)] * natural_vector.x + j[(0, 1)] * natural_vector.y + j[(0, 2)] * natural_vector.z, j[(1, 0)] * natural_vector.x + j[(1, 1)] * natural_vector.y + j[(1, 2)] * natural_vector.z, j[(2, 0)] * natural_vector.x + j[(2, 1)] * natural_vector.y + j[(2, 2)] * natural_vector.z, ); Ok(physical_vector) } _ => Err(ElementError::InvalidGeometry { message: format!( "Unsupported dimension combination: {spatial_dim}D spatial, {param_dim}D parametric" ), } .into()), } } /// Transform a tensor from natural to physical coordinates. pub fn transform_tensor( element: &dyn FiniteElement, coords: &NaturalCoords, node_coords: &[Vector3], natural_tensor: &nalgebra::DMatrix, ) -> FeaResult> { let jacobian_eval = element.jacobian(coords, node_coords)?; // T_physical = J * T_natural * J^T let j = &jacobian_eval.jacobian; let physical_tensor = j * natural_tensor * j.transpose(); Ok(physical_tensor) } /// Compute surface normal for boundary elements. pub fn surface_normal( element: &dyn FiniteElement, coords: &NaturalCoords, node_coords: &[Vector3], ) -> FeaResult> { let jacobian_eval = element.jacobian(coords, node_coords)?; let j = &jacobian_eval.jacobian; match element.parametric_dimension() { 1 => { // 1D boundary element in 2D/3D space if element.spatial_dimension() == 2 { let tangent = Vector3::new(j[(0, 0)], j[(1, 0)], 0.0); let normal = Vector3::new(-tangent.y, tangent.x, 0.0); Ok(normal.normalize()) } else { // For 1D element in 3D, normal is not uniquely defined Err(ElementError::InvalidGeometry { message: "Normal not uniquely defined for 1D element in 3D space" .to_string(), } .into()) } } 2 => { // 2D boundary element in 3D space if element.spatial_dimension() == 3 { let u = Vector3::new(j[(0, 0)], j[(1, 0)], j[(2, 0)]); let v = Vector3::new(j[(0, 1)], j[(1, 1)], j[(2, 1)]); let normal = u.cross(&v); Ok(normal.normalize()) } else { Err(ElementError::InvalidGeometry { message: "2D element in 2D space has no surface normal".to_string(), } .into()) } } _ => Err(ElementError::InvalidGeometry { message: format!( "Cannot compute surface normal for {}D element", element.parametric_dimension() ), } .into()), } } } #[cfg(disabled)] mod tests { use super::*; use crate::elements::shape_functions::*; #[test] fn test_mapping_quality_good_element() { let element = Triangle3::new(); let coords = NaturalCoords::new_2d(1.0 / 3.0, 1.0 / 3.0); // Well-shaped triangle let node_coords = vec![ Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 1.0, 0.0), ]; let quality = IsoparametricMapping::mapping_quality(&element, &coords, &node_coords).unwrap(); assert!(quality.is_acceptable()); assert!(quality.is_valid); assert!(quality.determinant > 0.0); assert!(quality.aspect_ratio < 2.0); assert!(quality.skewness < 0.1); } #[test] fn test_mapping_quality_distorted_element() { let element = Quadrilateral4::new(); let coords = NaturalCoords::new_2d(0.0, 0.0); // Highly distorted quadrilateral let node_coords = vec![ Vector3::new(0.0, 0.0, 0.0), Vector3::new(2.0, 0.0, 0.0), Vector3::new(1.9, 0.1, 0.0), // Nearly collapsed Vector3::new(0.1, 0.1, 0.0), ]; let quality = IsoparametricMapping::mapping_quality(&element, &coords, &node_coords).unwrap(); assert!(quality.determinant > 0.0); // Still valid but poor quality assert!(quality.aspect_ratio > 5.0); assert!(quality.skewness > 0.5); } #[test] fn test_element_mapping_analysis() { let element = Triangle3::new(); let node_coords = vec![ Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 1.0, 0.0), ]; let analysis = IsoparametricMapping::analyze_element_mapping(&element, &node_coords, 16).unwrap(); assert!(analysis.is_acceptable); assert_eq!(analysis.invalid_count, 0); assert!(analysis.min_determinant > 0.0); assert!(analysis.max_condition < 10.0); } #[test] fn test_coordinate_transform_vector() { let element = Triangle3::new(); let coords = NaturalCoords::new_2d(0.5, 0.25); let node_coords = vec![ Vector3::new(0.0, 0.0, 0.0), Vector3::new(2.0, 0.0, 0.0), // Scale by 2 in x Vector3::new(0.0, 1.0, 0.0), ]; let natural_vector = Vector3::new(1.0, 0.0, 0.0); // Unit vector in xi direction let physical_vector = CoordinateTransform::transform_vector(&element, &coords, &node_coords, &natural_vector) .unwrap(); // Should be scaled by the Jacobian assert!(physical_vector.x > 1.0); // Scaled by ~2 assert!((physical_vector.z).abs() < 1e-12); // z should remain 0 } #[test] fn test_surface_normal_2d_in_3d() { let element = Triangle3::new(); let coords = NaturalCoords::new_2d(1.0 / 3.0, 1.0 / 3.0); // Triangle in xy-plane let node_coords = vec![ Vector3::new(0.0, 0.0, 0.0), Vector3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 1.0, 0.0), ]; let normal = CoordinateTransform::surface_normal(&element, &coords, &node_coords).unwrap(); // Normal should point in +z direction assert!((normal.x).abs() < 1e-12); assert!((normal.y).abs() < 1e-12); assert!((normal.z - 1.0).abs() < 1e-12); } #[test] fn test_quality_score() { let good_quality = MappingQuality { determinant: 1.0, condition_number: 1.5, aspect_ratio: 1.2, skewness: 0.1, orthogonality: 0.9, is_valid: true, }; let poor_quality = MappingQuality { determinant: 0.01, condition_number: 100.0, aspect_ratio: 50.0, skewness: 0.7, orthogonality: 0.3, is_valid: true, }; assert!(good_quality.quality_score() > 0.8); assert!(poor_quality.quality_score() < 0.5); assert!(good_quality.is_acceptable()); assert!(!poor_quality.is_acceptable()); } #[test] fn test_sample_points_generation() { let triangle = Triangle3::new(); let quad = Quadrilateral4::new(); let tet = Tetrahedron4::new(); let hex = Hexahedron8::new(); let tri_points = IsoparametricMapping::generate_sample_points(&triangle, 16).unwrap(); let quad_points = IsoparametricMapping::generate_sample_points(&quad, 16).unwrap(); let tet_points = IsoparametricMapping::generate_sample_points(&tet, 27).unwrap(); let hex_points = IsoparametricMapping::generate_sample_points(&hex, 27).unwrap(); assert!(!tri_points.is_empty()); assert!(!quad_points.is_empty()); assert!(!tet_points.is_empty()); assert!(!hex_points.is_empty()); // Check that triangle points satisfy constraint for point in &tri_points { assert!(point.xi >= 0.0); assert!(point.eta >= 0.0); assert!(point.xi + point.eta <= 1.0 + 1e-12); } // Check that quad points are in bounds for point in &quad_points { assert!(point.xi >= -1.0 - 1e-12 && point.xi <= 1.0 + 1e-12); assert!(point.eta >= -1.0 - 1e-12 && point.eta <= 1.0 + 1e-12); } } #[test] fn test_aspect_ratio_computation() { let element = Quadrilateral4::new(); let coords = NaturalCoords::new_2d(0.0, 0.0); // Square element (aspect ratio should be ~1) let square_coords = vec![ Vector3::new(-1.0, -1.0, 0.0), Vector3::new(1.0, -1.0, 0.0), Vector3::new(1.0, 1.0, 0.0), Vector3::new(-1.0, 1.0, 0.0), ]; let quality = IsoparametricMapping::mapping_quality(&element, &coords, &square_coords).unwrap(); assert!((quality.aspect_ratio - 1.0).abs() < 0.1); // Rectangular element (high aspect ratio) let rect_coords = vec![ Vector3::new(-5.0, -1.0, 0.0), Vector3::new(5.0, -1.0, 0.0), Vector3::new(5.0, 1.0, 0.0), Vector3::new(-5.0, 1.0, 0.0), ]; let rect_quality = IsoparametricMapping::mapping_quality(&element, &coords, &rect_coords).unwrap(); assert!(rect_quality.aspect_ratio > 4.0); } }