//! Shared types for the StructuralPINN structural mechanics solver demo. //! //! This crate provides IPC types for physics-informed neural network based //! stress/strain analysis in structural mechanics applications. use serde::{Deserialize, Serialize}; // ============================================================================ // Geometry Types // ============================================================================ /// 2D point. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Point2D { pub x: f64, pub y: f64, } impl Point2D { pub fn new(x: f64, y: f64) -> Self { Self { x, y } } pub fn distance(&self, other: &Point2D) -> f64 { ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt() } pub fn origin() -> Self { Self { x: 0.0, y: 0.0 } } } /// 3D point. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Point3D { pub x: f64, pub y: f64, pub z: f64, } impl Point3D { pub fn new(x: f64, y: f64, z: f64) -> Self { Self { x, y, z } } pub fn distance(&self, other: &Point3D) -> f64 { ((self.x - other.x).powi(2) + (self.y - other.y).powi(2) + (self.z - other.z).powi(2)) .sqrt() } pub fn origin() -> Self { Self { x: 0.0, y: 0.0, z: 0.0, } } } // ============================================================================ // Material Properties // ============================================================================ /// Material properties for structural analysis. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Material { /// Young's modulus (elastic modulus) in Pa. pub youngs_modulus: f64, /// Poisson's ratio (dimensionless, typically 0.0-0.5). pub poisson_ratio: f64, /// Density in kg/m^3. pub density: f64, /// Yield stress in Pa (for plasticity analysis). pub yield_stress: f64, /// Thermal expansion coefficient in 1/K. pub thermal_expansion: f64, /// Material name for identification. #[serde(skip)] name: Option<&'static str>, } impl Material { /// Create a new material with specified properties. pub fn new(youngs_modulus: f64, poisson_ratio: f64, density: f64, yield_stress: f64) -> Self { Self { youngs_modulus, poisson_ratio, density, yield_stress, thermal_expansion: 0.0, name: None, } } /// Calculate shear modulus (G = E / (2 * (1 + nu))). pub fn shear_modulus(&self) -> f64 { self.youngs_modulus / (2.0 * (1.0 + self.poisson_ratio)) } /// Calculate bulk modulus (K = E / (3 * (1 - 2*nu))). pub fn bulk_modulus(&self) -> f64 { self.youngs_modulus / (3.0 * (1.0 - 2.0 * self.poisson_ratio)) } /// Calculate Lame's first parameter (lambda). pub fn lame_lambda(&self) -> f64 { let e = self.youngs_modulus; let nu = self.poisson_ratio; e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu)) } /// Calculate Lame's second parameter (mu = G). pub fn lame_mu(&self) -> f64 { self.shear_modulus() } /// Check if the material is compressible. pub fn is_compressible(&self) -> bool { self.poisson_ratio < 0.5 } } impl Default for Material { fn default() -> Self { Self::steel() } } impl Material { /// Structural steel (AISI 1020). pub fn steel() -> Self { Self { youngs_modulus: 200.0e9, poisson_ratio: 0.3, density: 7850.0, yield_stress: 350.0e6, thermal_expansion: 12.0e-6, name: Some("Steel AISI 1020"), } } /// Aluminum alloy (6061-T6). pub fn aluminum() -> Self { Self { youngs_modulus: 69.0e9, poisson_ratio: 0.33, density: 2700.0, yield_stress: 276.0e6, thermal_expansion: 23.6e-6, name: Some("Aluminum 6061-T6"), } } /// Titanium alloy (Ti-6Al-4V). pub fn titanium() -> Self { Self { youngs_modulus: 114.0e9, poisson_ratio: 0.34, density: 4430.0, yield_stress: 880.0e6, thermal_expansion: 8.6e-6, name: Some("Titanium Ti-6Al-4V"), } } /// Concrete (typical). pub fn concrete() -> Self { Self { youngs_modulus: 30.0e9, poisson_ratio: 0.2, density: 2400.0, yield_stress: 30.0e6, // Compressive strength thermal_expansion: 10.0e-6, name: Some("Concrete"), } } } // ============================================================================ // Geometry Definitions // ============================================================================ /// 2D geometry defined by vertices and element connectivity. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Geometry2D { /// Vertex coordinates. pub vertices: Vec, /// Element connectivity (indices into vertices). pub elements: Vec, /// Domain bounds. pub bounds: Bounds2D, } impl Geometry2D { /// Create a new 2D geometry. pub fn new(vertices: Vec, elements: Vec) -> Self { let bounds = Bounds2D::from_points(&vertices); Self { vertices, elements, bounds, } } /// Get number of vertices. pub fn num_vertices(&self) -> usize { self.vertices.len() } /// Get number of elements. pub fn num_elements(&self) -> usize { self.elements.len() } } /// 2D element types. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Element2D { /// Triangle element (3 nodes). Triangle([usize; 3]), /// Quadrilateral element (4 nodes). Quad([usize; 4]), /// 6-node quadratic triangle. Triangle6([usize; 6]), /// 8-node quadratic quad. Quad8([usize; 8]), } /// 3D geometry defined by vertices and element connectivity. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Geometry3D { /// Vertex coordinates. pub vertices: Vec, /// Element connectivity (indices into vertices). pub elements: Vec, /// Domain bounds. pub bounds: Bounds3D, } impl Geometry3D { /// Create a new 3D geometry. pub fn new(vertices: Vec, elements: Vec) -> Self { let bounds = Bounds3D::from_points(&vertices); Self { vertices, elements, bounds, } } /// Get number of vertices. pub fn num_vertices(&self) -> usize { self.vertices.len() } /// Get number of elements. pub fn num_elements(&self) -> usize { self.elements.len() } } /// 3D element types. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Element3D { /// Tetrahedron element (4 nodes). Tetrahedron([usize; 4]), /// Hexahedron (brick) element (8 nodes). Hexahedron([usize; 8]), /// Wedge (prism) element (6 nodes). Wedge([usize; 6]), /// Pyramid element (5 nodes). Pyramid([usize; 5]), /// 10-node quadratic tetrahedron. Tetrahedron10([usize; 10]), /// 20-node quadratic hexahedron. Hexahedron20([usize; 20]), } /// 2D domain bounds. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct Bounds2D { pub x_min: f64, pub x_max: f64, pub y_min: f64, pub y_max: f64, } impl Default for Bounds2D { fn default() -> Self { Self { x_min: 0.0, x_max: 1.0, y_min: 0.0, y_max: 1.0, } } } impl Bounds2D { /// Create bounds from a set of points. pub fn from_points(points: &[Point2D]) -> Self { if points.is_empty() { return Self::default(); } let mut bounds = Self { x_min: f64::MAX, x_max: f64::MIN, y_min: f64::MAX, y_max: f64::MIN, }; for p in points { bounds.x_min = bounds.x_min.min(p.x); bounds.x_max = bounds.x_max.max(p.x); bounds.y_min = bounds.y_min.min(p.y); bounds.y_max = bounds.y_max.max(p.y); } bounds } /// Get width (x extent). pub fn width(&self) -> f64 { self.x_max - self.x_min } /// Get height (y extent). pub fn height(&self) -> f64 { self.y_max - self.y_min } } /// 3D domain bounds. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct Bounds3D { pub x_min: f64, pub x_max: f64, pub y_min: f64, pub y_max: f64, pub z_min: f64, pub z_max: f64, } impl Default for Bounds3D { fn default() -> Self { Self { x_min: 0.0, x_max: 1.0, y_min: 0.0, y_max: 1.0, z_min: 0.0, z_max: 1.0, } } } impl Bounds3D { /// Create bounds from a set of points. pub fn from_points(points: &[Point3D]) -> Self { if points.is_empty() { return Self::default(); } let mut bounds = Self { x_min: f64::MAX, x_max: f64::MIN, y_min: f64::MAX, y_max: f64::MIN, z_min: f64::MAX, z_max: f64::MIN, }; for p in points { bounds.x_min = bounds.x_min.min(p.x); bounds.x_max = bounds.x_max.max(p.x); bounds.y_min = bounds.y_min.min(p.y); bounds.y_max = bounds.y_max.max(p.y); bounds.z_min = bounds.z_min.min(p.z); bounds.z_max = bounds.z_max.max(p.z); } bounds } } // ============================================================================ // Boundary Conditions // ============================================================================ /// Boundary condition type. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum BoundaryCondition { /// Dirichlet (displacement) boundary condition. Dirichlet { /// Node or face indices. nodes: Vec, /// Prescribed displacement values [u_x, u_y] or [u_x, u_y, u_z]. displacement: Vec, /// Which DOFs are constrained (true = fixed). constrained: Vec, }, /// Neumann (traction/force) boundary condition. Neumann { /// Node or face indices. nodes: Vec, /// Applied traction/force values. traction: Vec, }, /// Robin (mixed) boundary condition. Robin { /// Node or face indices. nodes: Vec, /// Stiffness coefficient. stiffness: f64, /// Reference displacement. reference: Vec, }, /// Symmetry boundary condition. Symmetry { /// Node indices on symmetry plane. nodes: Vec, /// Normal direction of symmetry plane. normal: Vec, }, /// Periodic boundary condition. Periodic { /// Master node indices. master_nodes: Vec, /// Slave node indices. slave_nodes: Vec, /// Translation vector. translation: Vec, }, } impl BoundaryCondition { /// Create a fixed (zero displacement) Dirichlet BC. pub fn fixed(nodes: Vec, ndim: usize) -> Self { Self::Dirichlet { nodes, displacement: vec![0.0; ndim], constrained: vec![true; ndim], } } /// Create a prescribed displacement Dirichlet BC. pub fn prescribed_displacement(nodes: Vec, displacement: Vec) -> Self { let constrained = vec![true; displacement.len()]; Self::Dirichlet { nodes, displacement, constrained, } } /// Create a traction (force per area) Neumann BC. pub fn traction(nodes: Vec, traction: Vec) -> Self { Self::Neumann { nodes, traction } } } // ============================================================================ // Loading Conditions // ============================================================================ /// Load type for structural analysis. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Load { /// Point load at specific nodes. Point { /// Node indices. nodes: Vec, /// Force vector [Fx, Fy] or [Fx, Fy, Fz] in N. force: Vec, }, /// Distributed load (force per length or area). Distributed { /// Element or edge indices. elements: Vec, /// Load intensity (force per unit length/area). intensity: Vec, /// Load direction (unit vector). direction: Vec, }, /// Pressure load (normal to surface). Pressure { /// Face indices. faces: Vec, /// Pressure magnitude in Pa (positive = compression). magnitude: f64, }, /// Body force (e.g., gravity). Body { /// Acceleration vector [ax, ay] or [ax, ay, az] in m/s^2. acceleration: Vec, }, /// Thermal load. Thermal { /// Temperature field at nodes (delta from reference). temperature: Vec, /// Reference temperature in K. reference_temp: f64, }, /// Centrifugal load. Centrifugal { /// Angular velocity in rad/s. omega: f64, /// Rotation axis (unit vector). axis: Vec, /// Point on rotation axis. center: Vec, }, } impl Load { /// Create a gravity load. pub fn gravity() -> Self { Self::Body { acceleration: vec![0.0, -9.81], } } /// Create a gravity load in 3D. pub fn gravity_3d() -> Self { Self::Body { acceleration: vec![0.0, 0.0, -9.81], } } /// Create a uniform pressure load. pub fn uniform_pressure(faces: Vec, pressure: f64) -> Self { Self::Pressure { faces, magnitude: pressure, } } } // ============================================================================ // Stress and Strain Fields // ============================================================================ /// Stress field results. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct StressField { /// Normal stress in x-direction (sigma_xx) at each point. pub sigma_xx: Vec, /// Normal stress in y-direction (sigma_yy) at each point. pub sigma_yy: Vec, /// Normal stress in z-direction (sigma_zz) at each point (3D only). pub sigma_zz: Vec, /// Shear stress (sigma_xy or tau_xy) at each point. pub sigma_xy: Vec, /// Shear stress (sigma_xz or tau_xz) at each point (3D only). pub sigma_xz: Vec, /// Shear stress (sigma_yz or tau_yz) at each point (3D only). pub sigma_yz: Vec, /// Von Mises equivalent stress at each point. pub von_mises: Vec, /// Maximum principal stress at each point. pub principal_max: Vec, /// Minimum principal stress at each point. pub principal_min: Vec, /// Hydrostatic (mean) stress at each point. pub hydrostatic: Vec, } impl StressField { /// Create a new stress field with given size. pub fn with_size(n: usize) -> Self { Self { sigma_xx: vec![0.0; n], sigma_yy: vec![0.0; n], sigma_zz: vec![0.0; n], sigma_xy: vec![0.0; n], sigma_xz: vec![0.0; n], sigma_yz: vec![0.0; n], von_mises: vec![0.0; n], principal_max: vec![0.0; n], principal_min: vec![0.0; n], hydrostatic: vec![0.0; n], } } /// Compute von Mises stress for 2D plane stress. pub fn compute_von_mises_2d(&mut self) { let n = self.sigma_xx.len(); self.von_mises = Vec::with_capacity(n); for i in 0..n { let sxx = self.sigma_xx[i]; let syy = self.sigma_yy[i]; let sxy = self.sigma_xy[i]; // Plane stress: sigma_zz = 0 let vm = (sxx.powi(2) + syy.powi(2) - sxx * syy + 3.0 * sxy.powi(2)).sqrt(); self.von_mises.push(vm); } } /// Compute von Mises stress for 3D. pub fn compute_von_mises_3d(&mut self) { let n = self.sigma_xx.len(); self.von_mises = Vec::with_capacity(n); for i in 0..n { let sxx = self.sigma_xx[i]; let syy = self.sigma_yy[i]; let szz = self.sigma_zz[i]; let sxy = self.sigma_xy[i]; let sxz = self.sigma_xz[i]; let syz = self.sigma_yz[i]; let vm = (0.5 * ((sxx - syy).powi(2) + (syy - szz).powi(2) + (szz - sxx).powi(2) + 6.0 * (sxy.powi(2) + sxz.powi(2) + syz.powi(2)))) .sqrt(); self.von_mises.push(vm); } } /// Get maximum von Mises stress. pub fn max_von_mises(&self) -> f64 { self.von_mises.iter().copied().fold(f64::MIN, f64::max) } } /// Displacement field results. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct DisplacementField { /// Displacement in x-direction at each node. pub u_x: Vec, /// Displacement in y-direction at each node. pub u_y: Vec, /// Displacement in z-direction at each node (3D only). pub u_z: Vec, /// Displacement magnitude at each node. pub magnitude: Vec, } impl DisplacementField { /// Create a new displacement field with given size. pub fn with_size(n: usize) -> Self { Self { u_x: vec![0.0; n], u_y: vec![0.0; n], u_z: vec![0.0; n], magnitude: vec![0.0; n], } } /// Compute displacement magnitude (2D). pub fn compute_magnitude_2d(&mut self) { let n = self.u_x.len(); self.magnitude = Vec::with_capacity(n); for i in 0..n { let mag = (self.u_x[i].powi(2) + self.u_y[i].powi(2)).sqrt(); self.magnitude.push(mag); } } /// Compute displacement magnitude (3D). pub fn compute_magnitude_3d(&mut self) { let n = self.u_x.len(); self.magnitude = Vec::with_capacity(n); for i in 0..n { let mag = (self.u_x[i].powi(2) + self.u_y[i].powi(2) + self.u_z[i].powi(2)).sqrt(); self.magnitude.push(mag); } } /// Get maximum displacement magnitude. pub fn max_displacement(&self) -> f64 { self.magnitude.iter().copied().fold(f64::MIN, f64::max) } } /// Strain field results. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct StrainField { /// Normal strain in x-direction (epsilon_xx). pub epsilon_xx: Vec, /// Normal strain in y-direction (epsilon_yy). pub epsilon_yy: Vec, /// Normal strain in z-direction (epsilon_zz) (3D only). pub epsilon_zz: Vec, /// Shear strain (gamma_xy = 2 * epsilon_xy). pub gamma_xy: Vec, /// Shear strain (gamma_xz = 2 * epsilon_xz) (3D only). pub gamma_xz: Vec, /// Shear strain (gamma_yz = 2 * epsilon_yz) (3D only). pub gamma_yz: Vec, /// Equivalent (von Mises) strain. pub equivalent: Vec, /// Volumetric strain. pub volumetric: Vec, } impl StrainField { /// Create a new strain field with given size. pub fn with_size(n: usize) -> Self { Self { epsilon_xx: vec![0.0; n], epsilon_yy: vec![0.0; n], epsilon_zz: vec![0.0; n], gamma_xy: vec![0.0; n], gamma_xz: vec![0.0; n], gamma_yz: vec![0.0; n], equivalent: vec![0.0; n], volumetric: vec![0.0; n], } } /// Compute volumetric strain. pub fn compute_volumetric(&mut self) { let n = self.epsilon_xx.len(); self.volumetric = Vec::with_capacity(n); for i in 0..n { let vol = self.epsilon_xx[i] + self.epsilon_yy[i] + self.epsilon_zz[i]; self.volumetric.push(vol); } } } // ============================================================================ // Analysis Configuration // ============================================================================ /// Analysis type. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum AnalysisType { /// Linear elastic static analysis. #[default] LinearStatic, /// Nonlinear static analysis (geometric/material nonlinearity). NonlinearStatic, /// Modal (eigenvalue) analysis. Modal, /// Dynamic (transient) analysis. Dynamic, /// Buckling analysis. Buckling, /// Steady-state thermal. Thermal, /// Fatigue analysis. Fatigue, } /// Plane stress/strain assumption for 2D. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum PlaneAssumption { /// Plane stress (thin structures, sigma_zz = 0). #[default] PlaneStress, /// Plane strain (thick structures, epsilon_zz = 0). PlaneStrain, /// Axisymmetric. Axisymmetric, } /// Analysis configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AnalysisConfig { /// Type of analysis. pub analysis_type: AnalysisType, /// Plane stress/strain assumption (for 2D). pub plane_assumption: PlaneAssumption, /// Maximum number of iterations (for nonlinear). pub max_iterations: usize, /// Convergence tolerance. pub tolerance: f64, /// Number of modes to compute (for modal analysis). pub num_modes: usize, /// Time step for dynamic analysis. pub time_step: f64, /// Total simulation time for dynamic analysis. pub total_time: f64, /// Enable large deformation (geometric nonlinearity). pub large_deformation: bool, /// Enable material nonlinearity (plasticity). pub material_nonlinearity: bool, /// Mesh refinement level. pub refinement_level: usize, } impl Default for AnalysisConfig { fn default() -> Self { Self { analysis_type: AnalysisType::LinearStatic, plane_assumption: PlaneAssumption::PlaneStress, max_iterations: 100, tolerance: 1e-6, num_modes: 10, time_step: 0.001, total_time: 1.0, large_deformation: false, material_nonlinearity: false, refinement_level: 1, } } } // ============================================================================ // Safety Factor // ============================================================================ /// Safety factor results. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SafetyFactor { /// Safety factor at each point (yield_stress / von_mises). pub values: Vec, /// Minimum safety factor. pub minimum: f64, /// Average safety factor. pub average: f64, /// Location of minimum safety factor (node index). pub min_location: usize, /// Percentage of domain with safety factor < 1.0. pub failure_percentage: f64, } impl Default for SafetyFactor { fn default() -> Self { Self { values: vec![], minimum: f64::MAX, average: 0.0, min_location: 0, failure_percentage: 0.0, } } } impl SafetyFactor { /// Compute safety factors from stress field and material. pub fn compute(stress: &StressField, material: &Material) -> Self { let yield_stress = material.yield_stress; let n = stress.von_mises.len(); if n == 0 { return Self::default(); } let mut values = Vec::with_capacity(n); let mut minimum = f64::MAX; let mut min_location = 0; let mut sum = 0.0; let mut failure_count = 0; for (i, &vm) in stress.von_mises.iter().enumerate() { let sf = if vm > 0.0 { yield_stress / vm } else { f64::MAX }; values.push(sf); sum += sf.min(100.0); // Cap for averaging if sf < minimum { minimum = sf; min_location = i; } if sf < 1.0 { failure_count += 1; } } Self { values, minimum, average: sum / n as f64, min_location, failure_percentage: 100.0 * failure_count as f64 / n as f64, } } /// Check if design is safe (minimum safety factor >= threshold). pub fn is_safe(&self, threshold: f64) -> bool { self.minimum >= threshold } } // ============================================================================ // Complete Analysis Result // ============================================================================ /// Complete structural analysis result. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AnalysisResult { /// Displacement field. pub displacement: DisplacementField, /// Stress field. pub stress: StressField, /// Strain field. pub strain: StrainField, /// Safety factor. pub safety_factor: SafetyFactor, /// Reaction forces at constrained DOFs. pub reaction_forces: Vec, /// Strain energy. pub strain_energy: f64, /// Natural frequencies (for modal analysis). pub natural_frequencies: Vec, /// Mode shapes (for modal analysis). pub mode_shapes: Vec>, /// Computation time in milliseconds. pub computation_time_ms: f64, /// Number of iterations (for nonlinear). pub iterations: usize, /// Convergence achieved. pub converged: bool, /// Physics residual (for PINN). pub physics_residual: f64, } impl Default for AnalysisResult { fn default() -> Self { Self { displacement: DisplacementField::default(), stress: StressField::default(), strain: StrainField::default(), safety_factor: SafetyFactor::default(), reaction_forces: vec![], strain_energy: 0.0, natural_frequencies: vec![], mode_shapes: vec![], computation_time_ms: 0.0, iterations: 0, converged: true, physics_residual: 0.0, } } } // ============================================================================ // PINN Configuration // ============================================================================ /// PINN network configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PinnConfig { /// Number of hidden layers. pub num_layers: usize, /// Hidden dimension. pub hidden_dim: usize, /// Activation function. pub activation: String, /// Learning rate. pub learning_rate: f64, /// Number of training epochs. pub epochs: usize, /// Batch size for training. pub batch_size: usize, /// Number of collocation points for physics loss. pub num_collocation_points: usize, /// Weight for physics loss. pub physics_weight: f64, /// Weight for boundary condition loss. pub bc_weight: f64, /// Weight for data loss. pub data_weight: f64, /// Enable adaptive loss weighting. pub adaptive_weights: bool, } impl Default for PinnConfig { fn default() -> Self { Self { num_layers: 4, hidden_dim: 64, activation: "tanh".to_string(), learning_rate: 1e-3, epochs: 1000, batch_size: 256, num_collocation_points: 10000, physics_weight: 1.0, bc_weight: 10.0, data_weight: 1.0, adaptive_weights: true, } } } /// Training progress for PINN. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TrainingProgress { pub epoch: usize, pub total_epochs: usize, pub physics_loss: f64, pub bc_loss: f64, pub data_loss: f64, pub total_loss: f64, pub learning_rate: f64, } // ============================================================================ // Sample Data Generation // ============================================================================ /// Create a simple rectangular mesh for 2D analysis. pub fn sample_rectangular_mesh(width: f64, height: f64, nx: usize, ny: usize) -> Geometry2D { let mut vertices = Vec::with_capacity((nx + 1) * (ny + 1)); let mut elements = Vec::with_capacity(nx * ny * 2); // Generate vertices for j in 0..=ny { for i in 0..=nx { let x = (i as f64 / nx as f64) * width; let y = (j as f64 / ny as f64) * height; vertices.push(Point2D::new(x, y)); } } // Generate triangular elements for j in 0..ny { for i in 0..nx { let n0 = j * (nx + 1) + i; let n1 = n0 + 1; let n2 = n0 + nx + 1; let n3 = n2 + 1; // Two triangles per quad elements.push(Element2D::Triangle([n0, n1, n2])); elements.push(Element2D::Triangle([n1, n3, n2])); } } Geometry2D::new(vertices, elements) } /// Create sample cantilever beam geometry. pub fn sample_cantilever_beam() -> Geometry2D { sample_rectangular_mesh(1.0, 0.1, 20, 4) } /// Create sample plate with hole geometry. pub fn sample_plate_with_hole( width: f64, height: f64, hole_radius: f64, nx: usize, ny: usize, ) -> Geometry2D { let mut vertices = Vec::new(); let mut elements = Vec::new(); let cx = width / 2.0; let cy = height / 2.0; // Generate vertices, excluding those inside hole for j in 0..=ny { for i in 0..=nx { let x = (i as f64 / nx as f64) * width; let y = (j as f64 / ny as f64) * height; let dx = x - cx; let dy = y - cy; let dist = (dx * dx + dy * dy).sqrt(); // Only add vertices outside the hole if dist > hole_radius { vertices.push(Point2D::new(x, y)); } } } // For simplicity, create a basic triangulation // (A real implementation would use proper hole meshing) let n = vertices.len(); if n >= 3 { for i in 0..n - 2 { elements.push(Element2D::Triangle([0, i + 1, i + 2])); } } Geometry2D::new(vertices, elements) } /// Create sample 3D brick mesh. pub fn sample_brick_mesh( length: f64, width: f64, height: f64, nx: usize, ny: usize, nz: usize, ) -> Geometry3D { let mut vertices = Vec::with_capacity((nx + 1) * (ny + 1) * (nz + 1)); let mut elements = Vec::with_capacity(nx * ny * nz); // Generate vertices for k in 0..=nz { for j in 0..=ny { for i in 0..=nx { let x = (i as f64 / nx as f64) * length; let y = (j as f64 / ny as f64) * width; let z = (k as f64 / nz as f64) * height; vertices.push(Point3D::new(x, y, z)); } } } // Generate hexahedral elements let stride_i = 1; let stride_j = nx + 1; let stride_k = (nx + 1) * (ny + 1); for k in 0..nz { for j in 0..ny { for i in 0..nx { let n0 = k * stride_k + j * stride_j + i * stride_i; let n1 = n0 + stride_i; let n2 = n0 + stride_j; let n3 = n2 + stride_i; let n4 = n0 + stride_k; let n5 = n4 + stride_i; let n6 = n4 + stride_j; let n7 = n6 + stride_i; elements.push(Element3D::Hexahedron([n0, n1, n3, n2, n4, n5, n7, n6])); } } } Geometry3D::new(vertices, elements) } // ============================================================================ // Tests // ============================================================================ #[cfg(test)] mod tests { use super::*; #[test] fn test_point2d() { let p1 = Point2D::new(0.0, 0.0); let p2 = Point2D::new(3.0, 4.0); assert!((p1.distance(&p2) - 5.0).abs() < 1e-10); } #[test] fn test_point3d() { let p1 = Point3D::origin(); let p2 = Point3D::new(1.0, 2.0, 2.0); assert!((p1.distance(&p2) - 3.0).abs() < 1e-10); } #[test] fn test_material_properties() { let steel = Material::steel(); assert!((steel.youngs_modulus - 200.0e9).abs() < 1.0); assert!((steel.poisson_ratio - 0.3).abs() < 0.01); let g = steel.shear_modulus(); assert!((g - 200.0e9 / 2.6).abs() < 1e6); let k = steel.bulk_modulus(); assert!(k > 0.0); } #[test] fn test_material_lame() { let mat = Material::new(1.0, 0.25, 1.0, 1.0); let lambda = mat.lame_lambda(); let mu = mat.lame_mu(); assert!(lambda > 0.0); assert!(mu > 0.0); } #[test] fn test_geometry2d() { let geom = sample_rectangular_mesh(1.0, 1.0, 2, 2); assert_eq!(geom.num_vertices(), 9); assert_eq!(geom.num_elements(), 8); // 2x2 quads = 4, each split into 2 triangles } #[test] fn test_geometry3d() { let geom = sample_brick_mesh(1.0, 1.0, 1.0, 2, 2, 2); assert_eq!(geom.num_vertices(), 27); // 3x3x3 assert_eq!(geom.num_elements(), 8); // 2x2x2 } #[test] fn test_bounds2d() { let points = vec![ Point2D::new(0.0, 0.0), Point2D::new(1.0, 0.0), Point2D::new(0.5, 1.0), ]; let bounds = Bounds2D::from_points(&points); assert!((bounds.x_min - 0.0).abs() < 1e-10); assert!((bounds.x_max - 1.0).abs() < 1e-10); assert!((bounds.y_max - 1.0).abs() < 1e-10); } #[test] fn test_boundary_condition() { let bc = BoundaryCondition::fixed(vec![0, 1, 2], 2); match bc { BoundaryCondition::Dirichlet { nodes, displacement, constrained, } => { assert_eq!(nodes.len(), 3); assert_eq!(displacement, vec![0.0, 0.0]); assert_eq!(constrained, vec![true, true]); } _ => panic!("Expected Dirichlet BC"), } } #[test] fn test_load() { let gravity = Load::gravity(); match gravity { Load::Body { acceleration } => { assert!((acceleration[1] + 9.81).abs() < 0.01); } _ => panic!("Expected Body load"), } } #[test] fn test_stress_field_von_mises() { let mut stress = StressField::with_size(2); stress.sigma_xx = vec![100.0, 200.0]; stress.sigma_yy = vec![50.0, 100.0]; stress.sigma_xy = vec![25.0, 50.0]; stress.compute_von_mises_2d(); assert_eq!(stress.von_mises.len(), 2); assert!(stress.von_mises[0] > 0.0); assert!(stress.von_mises[1] > stress.von_mises[0]); } #[test] fn test_displacement_field_magnitude() { let mut disp = DisplacementField::with_size(2); disp.u_x = vec![3.0, 0.0]; disp.u_y = vec![4.0, 5.0]; disp.compute_magnitude_2d(); assert!((disp.magnitude[0] - 5.0).abs() < 1e-10); assert!((disp.magnitude[1] - 5.0).abs() < 1e-10); } #[test] fn test_safety_factor() { let mut stress = StressField::with_size(3); stress.von_mises = vec![100.0e6, 200.0e6, 400.0e6]; let material = Material::steel(); // yield = 350 MPa let sf = SafetyFactor::compute(&stress, &material); assert!((sf.values[0] - 3.5).abs() < 0.01); assert!((sf.values[1] - 1.75).abs() < 0.01); assert!(sf.values[2] < 1.0); // Failure assert!(!sf.is_safe(1.0)); assert!(sf.failure_percentage > 30.0); } #[test] fn test_analysis_config() { let config = AnalysisConfig::default(); assert_eq!(config.analysis_type, AnalysisType::LinearStatic); assert_eq!(config.plane_assumption, PlaneAssumption::PlaneStress); } #[test] fn test_pinn_config() { let config = PinnConfig::default(); assert_eq!(config.num_layers, 4); assert_eq!(config.hidden_dim, 64); assert!(config.adaptive_weights); } #[test] fn test_serialization() { let material = Material::steel(); let json = serde_json::to_string(&material).unwrap(); let _: Material = serde_json::from_str(&json).unwrap(); let config = AnalysisConfig::default(); let json = serde_json::to_string(&config).unwrap(); let _: AnalysisConfig = serde_json::from_str(&json).unwrap(); } #[test] fn test_cantilever_beam() { let beam = sample_cantilever_beam(); assert!(beam.num_vertices() > 0); assert!(beam.num_elements() > 0); } }