CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
All 33 remaining #[cfg(disabled)] test modules outside the GPU cluster are now enabled: assembly (dof_mapping, constraints, global assembly), boundary (mod + dirichlet/neumann/robin/thermal/contact), analysis (mod + static), materials (mod, linear_elastic, hyperelastic, plasticity), elements (mod, element_matrices, isoparametric, jacobian, quadrature), mesh (element_types, connectivity, topology, topology_repair), solvers (mod, direct, iterative, nonlinear) and lib.rs. Lib tests 117 -> 335, stable across repeated runs. Only gpu_solver_tests and the GpuMeshData fixture stay disabled — they need CUDA hardware and belong to the GPU tranche. Three real defects found by the newly-compiling tests, each fixed: - Direct solvers reused factorizations keyed on matrix SIZE alone. In a Newton loop the Jacobian changes every iteration but never its dimension, so LuDirect/CholeskyDirect/LdltDirect silently solved with the first iteration's factorization forever — Newton on x^2-4 crawled to x=1.955 in 1000 iterations instead of converging in 5. Invisible in single-solve linear analysis, which is why every green test passed over it. solve() now factorizes the matrix it is given. - AdaptiveQuadrature's refinement re-integrated the WHOLE domain once per subdomain, so each level multiplied the estimate by the subdomain count: integrating e^x over [-1,1] at tolerance 1e-10 returned ~75 instead of 2.35. The recursion now descends into each sub-box with its share of the error budget. - compute_skewness read Jacobian columns as coordinate-line tangents, but the trait's jacobian() stores tangents in ROWS: on a sheared parallelogram whose tangents meet at 14 degrees it reported skewness 0.43 instead of 0.84 — measuring per-component gradients, not mesh skew. Fixtures corrected rather than the code where the fixture was wrong: sigma_yy ~ 0 asserted uniaxial-stress physics on a uniaxial-strain state (exact Lame values now asserted); an "unstable" orthotropic parameter set that satisfies the determinant stability condition (delta = 0.187 > 0); a unit-cube hex Jacobian of 1.0 that assumed a unit reference element (it is 0.125 from [-1,1]^3); a "distorted" quad whose centre Jacobian is exactly orthogonal, asserted as skewed (flattening and shearing now tested separately); a quality score below the implementation's own calibration; Rayleigh damping fed the scalar-field mass (now expanded via the Kronecker identity, with C = alpha*M + beta*K asserted entry-wise); an element factory required to construct Point/Line types that have no implementation; and DOF counts that encoded the repaired 3-DOFs-per-node-on-2-D defect. MaterialDatabase::add_material call sites updated to the (id, material, name) signature; ConnectivityInfo::build takes elements only; TopologyRepair::triangle_quality (normalized 4*sqrt(3)*A/sum(a^2)) added for the repair tests; create_subdomain_rule_* widened to pub(super) for the quadrature tests. Co-Authored-By: Claude Fable 5 <[email protected]>
187 lines
6.0 KiB
Rust
187 lines
6.0 KiB
Rust
// Copyright (c) 2024 RustyTorch++ Team
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
//! Plasticity models for finite element analysis.
|
|
|
|
use super::{Material, MaterialProperties, MaterialResponse, MaterialState};
|
|
use crate::error::FeaResult;
|
|
use nalgebra::{DMatrix, Vector6};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Von Mises plasticity with isotropic hardening.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct IsotropicPlasticity {
|
|
properties: MaterialProperties,
|
|
elastic_tangent: DMatrix<f64>,
|
|
yield_stress: f64,
|
|
hardening_modulus: f64,
|
|
}
|
|
|
|
impl IsotropicPlasticity {
|
|
/// Create a new isotropic plasticity material.
|
|
pub fn new(
|
|
elastic_modulus: f64,
|
|
poisson_ratio: f64,
|
|
yield_stress: f64,
|
|
hardening_modulus: f64,
|
|
) -> Self {
|
|
let properties =
|
|
MaterialProperties::isotropic_elastic(elastic_modulus, poisson_ratio, 7850.0);
|
|
|
|
// Elastic tangent matrix
|
|
let factor = elastic_modulus / ((1.0 + poisson_ratio) * (1.0 - 2.0 * poisson_ratio));
|
|
let mut d = DMatrix::zeros(6, 6);
|
|
|
|
d[(0, 0)] = factor * (1.0 - poisson_ratio);
|
|
d[(1, 1)] = factor * (1.0 - poisson_ratio);
|
|
d[(2, 2)] = factor * (1.0 - poisson_ratio);
|
|
d[(0, 1)] = factor * poisson_ratio;
|
|
d[(1, 0)] = d[(0, 1)];
|
|
d[(0, 2)] = factor * poisson_ratio;
|
|
d[(2, 0)] = d[(0, 2)];
|
|
d[(1, 2)] = factor * poisson_ratio;
|
|
d[(2, 1)] = d[(1, 2)];
|
|
d[(3, 3)] = factor * (1.0 - 2.0 * poisson_ratio) / 2.0;
|
|
d[(4, 4)] = factor * (1.0 - 2.0 * poisson_ratio) / 2.0;
|
|
d[(5, 5)] = factor * (1.0 - 2.0 * poisson_ratio) / 2.0;
|
|
|
|
Self {
|
|
properties,
|
|
elastic_tangent: d,
|
|
yield_stress,
|
|
hardening_modulus,
|
|
}
|
|
}
|
|
|
|
/// Set density.
|
|
pub fn with_density(mut self, density: f64) -> Self {
|
|
self.properties.density = density;
|
|
self
|
|
}
|
|
|
|
/// Compute von Mises stress.
|
|
fn von_mises_stress(&self, stress: &Vector6<f64>) -> f64 {
|
|
let s11 = stress[0];
|
|
let s22 = stress[1];
|
|
let s33 = stress[2];
|
|
let s12 = stress[3];
|
|
let s13 = stress[4];
|
|
let s23 = stress[5];
|
|
|
|
let diff1 = s11 - s22;
|
|
let diff2 = s22 - s33;
|
|
let diff3 = s33 - s11;
|
|
|
|
let vm_squared = 0.5 * (diff1 * diff1 + diff2 * diff2 + diff3 * diff3)
|
|
+ 3.0 * (s12 * s12 + s13 * s13 + s23 * s23);
|
|
|
|
vm_squared.sqrt()
|
|
}
|
|
|
|
/// Check yield condition.
|
|
fn yield_function(&self, stress: &Vector6<f64>, equivalent_plastic_strain: f64) -> f64 {
|
|
let vm_stress = self.von_mises_stress(stress);
|
|
let current_yield_stress =
|
|
self.yield_stress + self.hardening_modulus * equivalent_plastic_strain;
|
|
vm_stress - current_yield_stress
|
|
}
|
|
}
|
|
|
|
impl Material for IsotropicPlasticity {
|
|
fn properties(&self) -> &MaterialProperties {
|
|
&self.properties
|
|
}
|
|
|
|
fn compute_response(
|
|
&self,
|
|
strain_increment: &Vector6<f64>,
|
|
current_state: &MaterialState,
|
|
_dt: f64,
|
|
) -> FeaResult<MaterialResponse> {
|
|
let mut new_state = current_state.clone();
|
|
|
|
// Trial elastic step
|
|
let elastic_strain_increment = strain_increment;
|
|
let trial_stress = current_state.stress + &self.elastic_tangent * elastic_strain_increment;
|
|
|
|
// Check yield condition
|
|
let yield_value =
|
|
self.yield_function(&trial_stress, current_state.equivalent_plastic_strain);
|
|
|
|
if yield_value <= 0.0 {
|
|
// Elastic response
|
|
new_state.total_strain += strain_increment;
|
|
new_state.stress = trial_stress;
|
|
|
|
Ok(MaterialResponse::new(
|
|
trial_stress,
|
|
self.elastic_tangent.clone(),
|
|
new_state,
|
|
))
|
|
} else {
|
|
// Plastic response - return mapping
|
|
let vm_stress = self.von_mises_stress(&trial_stress);
|
|
let _current_yield_stress = self.yield_stress
|
|
+ self.hardening_modulus * current_state.equivalent_plastic_strain;
|
|
|
|
// Plastic multiplier
|
|
let shear_modulus = self.properties.shear_modulus();
|
|
let plastic_multiplier = yield_value / (3.0 * shear_modulus + self.hardening_modulus);
|
|
|
|
// Update plastic strain
|
|
let plastic_strain_increment = plastic_multiplier * 1.5 * trial_stress / vm_stress;
|
|
new_state.plastic_strain += plastic_strain_increment;
|
|
new_state.equivalent_plastic_strain += plastic_multiplier;
|
|
|
|
// Update stress
|
|
let stress_correction = &self.elastic_tangent * plastic_strain_increment;
|
|
new_state.stress = trial_stress - stress_correction;
|
|
new_state.total_strain += strain_increment;
|
|
|
|
// Consistent tangent (simplified)
|
|
let mut consistent_tangent = self.elastic_tangent.clone();
|
|
let beta = 3.0 * shear_modulus / (3.0 * shear_modulus + self.hardening_modulus);
|
|
consistent_tangent *= beta;
|
|
|
|
Ok(MaterialResponse::new(
|
|
new_state.stress,
|
|
consistent_tangent,
|
|
new_state,
|
|
))
|
|
}
|
|
}
|
|
|
|
fn elastic_tangent(&self) -> FeaResult<DMatrix<f64>> {
|
|
Ok(self.elastic_tangent.clone())
|
|
}
|
|
|
|
fn material_type(&self) -> &'static str {
|
|
"IsotropicPlasticity"
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_isotropic_plasticity_creation() {
|
|
let material = IsotropicPlasticity::new(200e9, 0.3, 250e6, 1e9);
|
|
assert_eq!(material.material_type(), "IsotropicPlasticity");
|
|
assert!(!material.is_linear());
|
|
}
|
|
|
|
#[test]
|
|
fn test_elastic_response() {
|
|
let material = IsotropicPlasticity::new(200e9, 0.3, 250e6, 1e9);
|
|
let small_strain = Vector6::new(0.0001, 0.0, 0.0, 0.0, 0.0, 0.0);
|
|
let state = MaterialState::default();
|
|
|
|
let response = material
|
|
.compute_response(&small_strain, &state, 1.0)
|
|
.unwrap();
|
|
assert!(response.is_valid);
|
|
assert!(!response.updated_state.has_yielded());
|
|
}
|
|
}
|