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]>
1294 lines
36 KiB
Rust
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);
|
|
}
|
|
}
|