Files
rustytorch/demos/structural-shared/src/lib.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

1294 lines
36 KiB
Rust

//! 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<Point2D>,
/// Element connectivity (indices into vertices).
pub elements: Vec<Element2D>,
/// Domain bounds.
pub bounds: Bounds2D,
}
impl Geometry2D {
/// Create a new 2D geometry.
pub fn new(vertices: Vec<Point2D>, elements: Vec<Element2D>) -> 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<Point3D>,
/// Element connectivity (indices into vertices).
pub elements: Vec<Element3D>,
/// Domain bounds.
pub bounds: Bounds3D,
}
impl Geometry3D {
/// Create a new 3D geometry.
pub fn new(vertices: Vec<Point3D>, elements: Vec<Element3D>) -> 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<usize>,
/// Prescribed displacement values [u_x, u_y] or [u_x, u_y, u_z].
displacement: Vec<f64>,
/// Which DOFs are constrained (true = fixed).
constrained: Vec<bool>,
},
/// Neumann (traction/force) boundary condition.
Neumann {
/// Node or face indices.
nodes: Vec<usize>,
/// Applied traction/force values.
traction: Vec<f64>,
},
/// Robin (mixed) boundary condition.
Robin {
/// Node or face indices.
nodes: Vec<usize>,
/// Stiffness coefficient.
stiffness: f64,
/// Reference displacement.
reference: Vec<f64>,
},
/// Symmetry boundary condition.
Symmetry {
/// Node indices on symmetry plane.
nodes: Vec<usize>,
/// Normal direction of symmetry plane.
normal: Vec<f64>,
},
/// Periodic boundary condition.
Periodic {
/// Master node indices.
master_nodes: Vec<usize>,
/// Slave node indices.
slave_nodes: Vec<usize>,
/// Translation vector.
translation: Vec<f64>,
},
}
impl BoundaryCondition {
/// Create a fixed (zero displacement) Dirichlet BC.
pub fn fixed(nodes: Vec<usize>, 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<usize>, displacement: Vec<f64>) -> 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<usize>, traction: Vec<f64>) -> 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<usize>,
/// Force vector [Fx, Fy] or [Fx, Fy, Fz] in N.
force: Vec<f64>,
},
/// Distributed load (force per length or area).
Distributed {
/// Element or edge indices.
elements: Vec<usize>,
/// Load intensity (force per unit length/area).
intensity: Vec<f64>,
/// Load direction (unit vector).
direction: Vec<f64>,
},
/// Pressure load (normal to surface).
Pressure {
/// Face indices.
faces: Vec<usize>,
/// 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<f64>,
},
/// Thermal load.
Thermal {
/// Temperature field at nodes (delta from reference).
temperature: Vec<f64>,
/// Reference temperature in K.
reference_temp: f64,
},
/// Centrifugal load.
Centrifugal {
/// Angular velocity in rad/s.
omega: f64,
/// Rotation axis (unit vector).
axis: Vec<f64>,
/// Point on rotation axis.
center: Vec<f64>,
},
}
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<usize>, 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<f64>,
/// Normal stress in y-direction (sigma_yy) at each point.
pub sigma_yy: Vec<f64>,
/// Normal stress in z-direction (sigma_zz) at each point (3D only).
pub sigma_zz: Vec<f64>,
/// Shear stress (sigma_xy or tau_xy) at each point.
pub sigma_xy: Vec<f64>,
/// Shear stress (sigma_xz or tau_xz) at each point (3D only).
pub sigma_xz: Vec<f64>,
/// Shear stress (sigma_yz or tau_yz) at each point (3D only).
pub sigma_yz: Vec<f64>,
/// Von Mises equivalent stress at each point.
pub von_mises: Vec<f64>,
/// Maximum principal stress at each point.
pub principal_max: Vec<f64>,
/// Minimum principal stress at each point.
pub principal_min: Vec<f64>,
/// Hydrostatic (mean) stress at each point.
pub hydrostatic: Vec<f64>,
}
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<f64>,
/// Displacement in y-direction at each node.
pub u_y: Vec<f64>,
/// Displacement in z-direction at each node (3D only).
pub u_z: Vec<f64>,
/// Displacement magnitude at each node.
pub magnitude: Vec<f64>,
}
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<f64>,
/// Normal strain in y-direction (epsilon_yy).
pub epsilon_yy: Vec<f64>,
/// Normal strain in z-direction (epsilon_zz) (3D only).
pub epsilon_zz: Vec<f64>,
/// Shear strain (gamma_xy = 2 * epsilon_xy).
pub gamma_xy: Vec<f64>,
/// Shear strain (gamma_xz = 2 * epsilon_xz) (3D only).
pub gamma_xz: Vec<f64>,
/// Shear strain (gamma_yz = 2 * epsilon_yz) (3D only).
pub gamma_yz: Vec<f64>,
/// Equivalent (von Mises) strain.
pub equivalent: Vec<f64>,
/// Volumetric strain.
pub volumetric: Vec<f64>,
}
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<f64>,
/// 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<f64>,
/// Strain energy.
pub strain_energy: f64,
/// Natural frequencies (for modal analysis).
pub natural_frequencies: Vec<f64>,
/// Mode shapes (for modal analysis).
pub mode_shapes: Vec<Vec<f64>>,
/// 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);
}
}