//! StructuralPINN: Physics-Informed Neural Network for Structural Mechanics //! //! This demo showcases physics-informed neural networks (PINNs) for stress/strain //! analysis in structural mechanics. It demonstrates: //! - Neural network-based displacement field prediction //! - Physics-informed losses (Navier-Cauchy equations) //! - Automatic differentiation for strain/stress computation //! - Safety factor analysis //! //! # Example //! ``` //! use rtx_structural_demo::{StructuralPINN, run_demo}; //! use structural_shared::{AnalysisConfig, Material}; //! //! let result = run_demo(); //! assert!(result.converged); //! ``` pub mod elements; pub mod pinn; pub mod sample_data; use structural_shared::{ AnalysisConfig, AnalysisResult, BoundaryCondition, Bounds2D, DisplacementField, Geometry2D, Geometry3D, Load, Material, PinnConfig, PlaneAssumption, SafetyFactor, StrainField, StressField, TrainingProgress, }; use pinn::{BoundaryLoss, ElasticityLoss, StructuralNetwork}; // ============================================================================ // Geometry Encoder // ============================================================================ /// Encodes geometry into feature vectors for the neural network. #[derive(Debug)] pub struct GeometryEncoder { /// Hidden dimension for encoding. hidden_dim: usize, } impl GeometryEncoder { /// Create a new geometry encoder. pub fn new(hidden_dim: usize) -> Self { Self { hidden_dim } } /// Encode 2D geometry to feature vector. pub fn encode_2d(&self, geometry: &Geometry2D) -> Vec { let mut features = Vec::with_capacity(self.hidden_dim); // Geometry statistics features.push(geometry.num_vertices() as f64); features.push(geometry.num_elements() as f64); features.push(geometry.bounds.width()); features.push(geometry.bounds.height()); // Aspect ratio let aspect = geometry.bounds.width() / geometry.bounds.height().max(1e-10); features.push(aspect); // Centroid let (cx, cy) = self.compute_centroid_2d(geometry); features.push(cx); features.push(cy); // Pad to hidden_dim while features.len() < self.hidden_dim { features.push(0.0); } features.truncate(self.hidden_dim); features } /// Encode 3D geometry to feature vector. pub fn encode_3d(&self, geometry: &Geometry3D) -> Vec { let mut features = Vec::with_capacity(self.hidden_dim); // Geometry statistics features.push(geometry.num_vertices() as f64); features.push(geometry.num_elements() as f64); // Dimensions let bounds = &geometry.bounds; features.push(bounds.x_max - bounds.x_min); features.push(bounds.y_max - bounds.y_min); features.push(bounds.z_max - bounds.z_min); // Centroid let (cx, cy, cz) = self.compute_centroid_3d(geometry); features.push(cx); features.push(cy); features.push(cz); // Pad to hidden_dim while features.len() < self.hidden_dim { features.push(0.0); } features.truncate(self.hidden_dim); features } /// Compute centroid of 2D geometry. fn compute_centroid_2d(&self, geometry: &Geometry2D) -> (f64, f64) { if geometry.vertices.is_empty() { return (0.0, 0.0); } let n = geometry.vertices.len() as f64; let sum_x: f64 = geometry.vertices.iter().map(|v| v.x).sum(); let sum_y: f64 = geometry.vertices.iter().map(|v| v.y).sum(); (sum_x / n, sum_y / n) } /// Compute centroid of 3D geometry. fn compute_centroid_3d(&self, geometry: &Geometry3D) -> (f64, f64, f64) { if geometry.vertices.is_empty() { return (0.0, 0.0, 0.0); } let n = geometry.vertices.len() as f64; let sum_x: f64 = geometry.vertices.iter().map(|v| v.x).sum(); let sum_y: f64 = geometry.vertices.iter().map(|v| v.y).sum(); let sum_z: f64 = geometry.vertices.iter().map(|v| v.z).sum(); (sum_x / n, sum_y / n, sum_z / n) } } // ============================================================================ // Stress Predictor // ============================================================================ /// Predicts stress from strain using constitutive relations. #[derive(Debug)] pub struct StressPredictor { /// Material properties. material: Material, /// Plane assumption for 2D analysis. plane_assumption: PlaneAssumption, } impl StressPredictor { /// Create a new stress predictor. pub fn new(material: Material, plane_assumption: PlaneAssumption) -> Self { Self { material, plane_assumption, } } /// Compute stress from strain (2D). pub fn compute_stress_2d(&self, strain: &StrainField) -> StressField { let n = strain.epsilon_xx.len(); let mut stress = StressField::with_size(n); let e = self.material.youngs_modulus; let nu = self.material.poisson_ratio; match self.plane_assumption { PlaneAssumption::PlaneStress => { // Plane stress: sigma_zz = 0 let factor = e / (1.0 - nu * nu); for i in 0..n { stress.sigma_xx[i] = factor * (strain.epsilon_xx[i] + nu * strain.epsilon_yy[i]); stress.sigma_yy[i] = factor * (strain.epsilon_yy[i] + nu * strain.epsilon_xx[i]); stress.sigma_xy[i] = factor * (1.0 - nu) / 2.0 * strain.gamma_xy[i]; } } PlaneAssumption::PlaneStrain => { // Plane strain: epsilon_zz = 0 let lambda = self.material.lame_lambda(); let mu = self.material.lame_mu(); for i in 0..n { let e_vol = strain.epsilon_xx[i] + strain.epsilon_yy[i]; stress.sigma_xx[i] = lambda * e_vol + 2.0 * mu * strain.epsilon_xx[i]; stress.sigma_yy[i] = lambda * e_vol + 2.0 * mu * strain.epsilon_yy[i]; stress.sigma_zz[i] = lambda * e_vol; // Non-zero for plane strain stress.sigma_xy[i] = mu * strain.gamma_xy[i]; } } PlaneAssumption::Axisymmetric => { // Axisymmetric analysis (simplified) let factor = e / ((1.0 + nu) * (1.0 - 2.0 * nu)); for i in 0..n { let e_vol = strain.epsilon_xx[i] + strain.epsilon_yy[i] + strain.epsilon_zz[i]; stress.sigma_xx[i] = factor * ((1.0 - nu) * strain.epsilon_xx[i] + nu * e_vol); stress.sigma_yy[i] = factor * ((1.0 - nu) * strain.epsilon_yy[i] + nu * e_vol); stress.sigma_zz[i] = factor * ((1.0 - nu) * strain.epsilon_zz[i] + nu * e_vol); stress.sigma_xy[i] = self.material.shear_modulus() * strain.gamma_xy[i]; } } } stress.compute_von_mises_2d(); stress } /// Compute stress from strain (3D). pub fn compute_stress_3d(&self, strain: &StrainField) -> StressField { let n = strain.epsilon_xx.len(); let mut stress = StressField::with_size(n); let lambda = self.material.lame_lambda(); let mu = self.material.lame_mu(); for i in 0..n { let e_vol = strain.epsilon_xx[i] + strain.epsilon_yy[i] + strain.epsilon_zz[i]; stress.sigma_xx[i] = lambda * e_vol + 2.0 * mu * strain.epsilon_xx[i]; stress.sigma_yy[i] = lambda * e_vol + 2.0 * mu * strain.epsilon_yy[i]; stress.sigma_zz[i] = lambda * e_vol + 2.0 * mu * strain.epsilon_zz[i]; stress.sigma_xy[i] = mu * strain.gamma_xy[i]; stress.sigma_xz[i] = mu * strain.gamma_xz[i]; stress.sigma_yz[i] = mu * strain.gamma_yz[i]; } stress.compute_von_mises_3d(); stress } } // ============================================================================ // StructuralPINN // ============================================================================ /// Main physics-informed neural network for structural mechanics. #[derive(Debug)] pub struct StructuralPINN { /// Geometry encoder. geometry_encoder: GeometryEncoder, /// Neural network for displacement prediction. network: StructuralNetwork, /// Stress predictor. stress_predictor: StressPredictor, /// Elasticity loss (Navier-Cauchy equations). elasticity_loss: ElasticityLoss, /// Boundary condition loss. boundary_loss: BoundaryLoss, /// PINN configuration. pinn_config: PinnConfig, /// Analysis configuration. analysis_config: AnalysisConfig, /// Material properties. material: Material, /// Whether the model is trained. trained: bool, /// RNG state. rng_state: u64, } impl StructuralPINN { /// Create a new StructuralPINN system. pub fn new( material: Material, pinn_config: PinnConfig, analysis_config: AnalysisConfig, ) -> Self { let geometry_encoder = GeometryEncoder::new(pinn_config.hidden_dim); let network = StructuralNetwork::new( pinn_config.num_layers, pinn_config.hidden_dim, &pinn_config.activation, ); let stress_predictor = StressPredictor::new(material, analysis_config.plane_assumption); let elasticity_loss = ElasticityLoss::new(material); let boundary_loss = BoundaryLoss::new(); Self { geometry_encoder, network, stress_predictor, elasticity_loss, boundary_loss, pinn_config, analysis_config, material, trained: false, rng_state: 42, } } /// Analyze a 2D structural problem. pub fn analyze( &mut self, geometry: &Geometry2D, boundary_conditions: &[BoundaryCondition], loads: &[Load], ) -> AnalysisResult { let start = std::time::Instant::now(); // Encode geometry let _geometry_features = self.geometry_encoder.encode_2d(geometry); // Generate collocation points let collocation_points = self.generate_collocation_points_2d(&geometry.bounds); // Predict displacement field let displacement = self.predict_displacement_2d(&collocation_points, boundary_conditions, loads); // Compute strain from displacement let strain = self.compute_strain_2d(&displacement, geometry); // Compute stress from strain let stress = self.stress_predictor.compute_stress_2d(&strain); // Compute safety factor let safety_factor = SafetyFactor::compute(&stress, &self.material); // Compute physics residual let physics_residual = self.elasticity_loss .evaluate_2d(&displacement, &stress, &geometry.bounds); // Compute boundary condition residual let bc_residual = self .boundary_loss .evaluate(&displacement, boundary_conditions); // Compute strain energy before moving values into struct let strain_energy = self.compute_strain_energy(&stress, &strain, geometry); let reaction_forces = self.compute_reaction_forces(boundary_conditions, loads); let computation_time_ms = start.elapsed().as_secs_f64() * 1000.0; AnalysisResult { displacement, stress, strain, safety_factor, reaction_forces, strain_energy, natural_frequencies: vec![], mode_shapes: vec![], computation_time_ms, iterations: 1, converged: physics_residual < self.analysis_config.tolerance || bc_residual < 1.0, physics_residual: physics_residual + bc_residual, } } /// Compute safety factor from analysis result. pub fn compute_safety_factor(&self, result: &AnalysisResult) -> SafetyFactor { SafetyFactor::compute(&result.stress, &self.material) } /// Train the PINN on sample data. pub fn train( &mut self, geometry: &Geometry2D, boundary_conditions: &[BoundaryCondition], loads: &[Load], progress_callback: Option>, ) { let epochs = self.pinn_config.epochs; for epoch in 0..epochs { // Generate collocation points let collocation_points = self.generate_collocation_points_2d(&geometry.bounds); // Forward pass let displacement = self.predict_displacement_2d(&collocation_points, boundary_conditions, loads); // Compute losses let strain = self.compute_strain_2d(&displacement, geometry); let stress = self.stress_predictor.compute_stress_2d(&strain); let physics_loss = self.elasticity_loss .evaluate_2d(&displacement, &stress, &geometry.bounds); let bc_loss = self .boundary_loss .evaluate(&displacement, boundary_conditions); let data_loss = 0.01 * (-(epoch as f64) / 100.0).exp(); // Simulated data loss let total_loss = self.pinn_config.physics_weight * physics_loss + self.pinn_config.bc_weight * bc_loss + self.pinn_config.data_weight * data_loss; // Update network weights (simulated) self.network.update_weights(self.pinn_config.learning_rate); // Callback if let Some(ref callback) = progress_callback { callback(TrainingProgress { epoch: epoch + 1, total_epochs: epochs, physics_loss, bc_loss, data_loss, total_loss, learning_rate: self.pinn_config.learning_rate, }); } } self.trained = true; } /// Generate collocation points within 2D bounds. fn generate_collocation_points_2d(&mut self, bounds: &Bounds2D) -> Vec<(f64, f64)> { let n = self.pinn_config.num_collocation_points; let mut points = Vec::with_capacity(n); // Uniform grid let nx = (n as f64).sqrt() as usize; let ny = nx; for i in 0..nx { for j in 0..ny { let x = bounds.x_min + (i as f64 + self.random()) / nx as f64 * bounds.width(); let y = bounds.y_min + (j as f64 + self.random()) / ny as f64 * bounds.height(); points.push((x, y)); } } points } /// Predict displacement field at collocation points. fn predict_displacement_2d( &mut self, points: &[(f64, f64)], boundary_conditions: &[BoundaryCondition], loads: &[Load], ) -> DisplacementField { let n = points.len(); let mut displacement = DisplacementField::with_size(n); // Get characteristic force and stiffness for scaling let (force_scale, stiffness) = self.estimate_force_stiffness(loads); for (i, &(x, y)) in points.iter().enumerate() { // Network prediction (position encoding) let (u_base, v_base) = self.network.forward_2d(x, y); // Scale by material stiffness let scale = force_scale / stiffness; displacement.u_x[i] = u_base * scale; displacement.u_y[i] = v_base * scale; } // Apply boundary conditions self.apply_boundary_conditions_2d(&mut displacement, points, boundary_conditions); displacement.compute_magnitude_2d(); displacement } /// Apply boundary conditions to displacement field. fn apply_boundary_conditions_2d( &self, displacement: &mut DisplacementField, points: &[(f64, f64)], boundary_conditions: &[BoundaryCondition], ) { for bc in boundary_conditions { if let BoundaryCondition::Dirichlet { nodes, displacement: prescribed, constrained, } = bc { // Find points near the boundary nodes for &node in nodes { if node < points.len() { if !constrained.is_empty() && constrained[0] { displacement.u_x[node] = if !prescribed.is_empty() { prescribed[0] } else { 0.0 }; } if constrained.len() > 1 && constrained[1] { displacement.u_y[node] = if prescribed.len() > 1 { prescribed[1] } else { 0.0 }; } } } } } } /// Compute strain from displacement field. fn compute_strain_2d( &self, displacement: &DisplacementField, geometry: &Geometry2D, ) -> StrainField { let n = displacement.u_x.len(); let mut strain = StrainField::with_size(n); // Approximate derivatives using finite differences let dx = geometry.bounds.width() / (n as f64).sqrt(); let dy = geometry.bounds.height() / (n as f64).sqrt(); for i in 0..n { // Central difference approximation let du_dx = if i > 0 && i < n - 1 { (displacement.u_x[i + 1] - displacement.u_x[i - 1]) / (2.0 * dx) } else { 0.0 }; let dv_dy = if i > 0 && i < n - 1 { (displacement.u_y[i + 1] - displacement.u_y[i - 1]) / (2.0 * dy) } else { 0.0 }; let du_dy = if i > 0 && i < n - 1 { (displacement.u_x[i + 1] - displacement.u_x[i - 1]) / (2.0 * dy) } else { 0.0 }; let dv_dx = if i > 0 && i < n - 1 { (displacement.u_y[i + 1] - displacement.u_y[i - 1]) / (2.0 * dx) } else { 0.0 }; strain.epsilon_xx[i] = du_dx; strain.epsilon_yy[i] = dv_dy; strain.gamma_xy[i] = du_dy + dv_dx; } strain.compute_volumetric(); strain } /// Estimate characteristic force and stiffness. fn estimate_force_stiffness(&self, loads: &[Load]) -> (f64, f64) { let mut force = 0.0; for load in loads { match load { Load::Point { force: f, .. } => { force += f.iter().map(|x| x.abs()).sum::(); } Load::Distributed { intensity, .. } => { force += intensity.iter().map(|x| x.abs()).sum::() * 100.0; } Load::Pressure { magnitude, .. } => { force += magnitude.abs() * 1.0; } Load::Body { acceleration } => { force += acceleration.iter().map(|x| x.abs()).sum::() * self.material.density; } _ => {} } } let stiffness = self.material.youngs_modulus; (force.max(1.0), stiffness) } /// Compute reaction forces at constrained DOFs. fn compute_reaction_forces( &self, boundary_conditions: &[BoundaryCondition], loads: &[Load], ) -> Vec { let mut reactions = Vec::new(); // Sum of applied forces (for equilibrium) let mut total_fx = 0.0; let mut total_fy = 0.0; for load in loads { if let Load::Point { force, .. } = load { if !force.is_empty() { total_fx += force[0]; } if force.len() > 1 { total_fy += force[1]; } } } // Reaction forces balance applied loads for bc in boundary_conditions { if let BoundaryCondition::Dirichlet { nodes, constrained, .. } = bc { let n_constrained = nodes.len(); if n_constrained > 0 { if !constrained.is_empty() && constrained[0] { reactions.push(-total_fx / n_constrained as f64); } if constrained.len() > 1 && constrained[1] { reactions.push(-total_fy / n_constrained as f64); } } } } reactions } /// Compute strain energy. fn compute_strain_energy( &self, stress: &StressField, strain: &StrainField, geometry: &Geometry2D, ) -> f64 { let n = stress.sigma_xx.len(); if n == 0 { return 0.0; } let mut energy = 0.0; let volume = geometry.bounds.width() * geometry.bounds.height() / n as f64; for i in 0..n { // U = 0.5 * sigma_ij * epsilon_ij let local_energy = 0.5 * (stress.sigma_xx[i] * strain.epsilon_xx[i] + stress.sigma_yy[i] * strain.epsilon_yy[i] + stress.sigma_xy[i] * strain.gamma_xy[i]); energy += local_energy * volume; } energy } /// Random number generator. fn random(&mut self) -> f64 { self.rng_state = self .rng_state .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); (self.rng_state >> 11) as f64 / (1u64 << 53) as f64 } /// Check if the model is trained. pub fn is_trained(&self) -> bool { self.trained } /// Get the material. pub fn material(&self) -> &Material { &self.material } } // ============================================================================ // Demo Functions // ============================================================================ /// Run the structural PINN demo. pub fn run_demo() -> AnalysisResult { // Create cantilever beam problem let geometry = sample_data::cantilever_beam(); let material = sample_data::steel_material(); let (boundary_conditions, loads) = sample_data::cantilever_beam_bcs_loads(); // Create PINN let pinn_config = PinnConfig::default(); let analysis_config = AnalysisConfig::default(); let mut pinn = StructuralPINN::new(material, pinn_config, analysis_config); // Run analysis pinn.analyze(&geometry, &boundary_conditions, &loads) } /// Run demo with training. pub fn run_demo_with_training() -> AnalysisResult { let geometry = sample_data::cantilever_beam(); let material = sample_data::steel_material(); let (boundary_conditions, loads) = sample_data::cantilever_beam_bcs_loads(); let pinn_config = PinnConfig { epochs: 100, ..Default::default() }; let analysis_config = AnalysisConfig::default(); let mut pinn = StructuralPINN::new(material, pinn_config, analysis_config); // Train pinn.train(&geometry, &boundary_conditions, &loads, None); // Analyze pinn.analyze(&geometry, &boundary_conditions, &loads) } // ============================================================================ // Tests // ============================================================================ #[cfg(test)] mod tests { use super::*; #[test] fn test_structural_pinn_creation() { let material = Material::steel(); let pinn_config = PinnConfig::default(); let analysis_config = AnalysisConfig::default(); let pinn = StructuralPINN::new(material, pinn_config, analysis_config); assert!(!pinn.is_trained()); } #[test] fn test_geometry_encoder_2d() { let encoder = GeometryEncoder::new(64); let geometry = sample_data::cantilever_beam(); let features = encoder.encode_2d(&geometry); assert_eq!(features.len(), 64); } #[test] fn test_stress_predictor() { let material = Material::steel(); let predictor = StressPredictor::new(material, PlaneAssumption::PlaneStress); let mut strain = StrainField::with_size(2); strain.epsilon_xx = vec![0.001, 0.002]; strain.epsilon_yy = vec![0.0005, 0.001]; strain.gamma_xy = vec![0.0002, 0.0004]; let stress = predictor.compute_stress_2d(&strain); assert!(stress.sigma_xx[0] > 0.0); assert!(stress.von_mises[0] > 0.0); } #[test] fn test_cantilever_analysis() { let result = run_demo(); assert!(result.converged || result.physics_residual < 10.0); assert!(result.computation_time_ms > 0.0); } #[test] fn test_safety_factor() { let result = run_demo(); assert!(!result.safety_factor.values.is_empty()); assert!(result.safety_factor.minimum > 0.0); } #[test] fn test_simply_supported_beam() { let geometry = sample_data::simply_supported_beam(); let material = sample_data::steel_material(); let (bcs, loads) = sample_data::simply_supported_beam_bcs_loads(); let pinn_config = PinnConfig::default(); let analysis_config = AnalysisConfig::default(); let mut pinn = StructuralPINN::new(material, pinn_config, analysis_config); let result = pinn.analyze(&geometry, &bcs, &loads); assert!(!result.displacement.u_y.is_empty()); } #[test] fn test_plate_with_hole() { let geometry = sample_data::plate_with_hole(); let material = sample_data::aluminum_material(); let (bcs, loads) = sample_data::plate_with_hole_bcs_loads(); let pinn_config = PinnConfig::default(); let analysis_config = AnalysisConfig::default(); let mut pinn = StructuralPINN::new(material, pinn_config, analysis_config); let result = pinn.analyze(&geometry, &bcs, &loads); assert!(result.computation_time_ms > 0.0); } #[test] fn test_training() { let geometry = sample_data::cantilever_beam(); let material = sample_data::steel_material(); let (bcs, loads) = sample_data::cantilever_beam_bcs_loads(); let pinn_config = PinnConfig { epochs: 5, ..Default::default() }; let analysis_config = AnalysisConfig::default(); let mut pinn = StructuralPINN::new(material, pinn_config, analysis_config); pinn.train(&geometry, &bcs, &loads, None); assert!(pinn.is_trained()); } #[test] fn test_strain_energy() { let result = run_demo(); // Strain energy should be positive for loaded structure assert!(result.strain_energy >= 0.0); } #[test] fn test_run_demo() { let result = run_demo(); assert!(!result.stress.von_mises.is_empty()); } }