rtx-fea: mor gains total-Lagrangian operators and a held-out ECSW residual
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
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
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
The mor scope was small-strain only ("geometrically linear") — a basis
trained on total-Lagrangian trajectories (the FSI flag marches
with_total_lagrangian) sampled through small-strain operators would
conserve the virtual work of the wrong force. ElementOperator now
carries a Formulation (SmallStrain | TotalLagrangian), the TL branch
mirroring NonlinearDynamicAnalysis exactly (SVK from Lame parameters,
total_lagrangian::internal_force_and_tangent); train_ecsw /
ReducedNonlinearModel::new keep their behavior and delegate, with
_formulated variants added. ecsw_residual evaluates a trained model's
||Cw - b||/||b|| on arbitrary snapshots — the held-out generalization
measurement; on the training set it reproduces training_residual to
1e-12 (pinned).
Verified sharply: identity-basis reduced TL solve vs a
tight-tolerance full TL solve agrees to 4.4e-15 (machine precision)
while small-strain operators land 1.1e-2 away at the same load — the
switch is exercised and exact. (At the default 1e-6 convergence
criteria the reference itself stops 1.7e-4 short; measured and
recorded in the test comment.)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
co-authored by
Claude Fable 5
parent
9fe9d7f74a
commit
d9d8801f1a
@@ -20,13 +20,36 @@
|
||||
//! negative weight would let a sampled element *produce* energy.
|
||||
|
||||
use super::nnls::nnls;
|
||||
use super::reduced::ElementOperator;
|
||||
use super::reduced::{ElementOperator, Formulation};
|
||||
use crate::assembly::dof_mapping::AdvancedDofNumbering;
|
||||
use crate::error::{FeaError, FeaResult};
|
||||
use crate::materials::MaterialDatabase;
|
||||
use crate::mesh::{ElementId, Mesh};
|
||||
use nalgebra::{DMatrix, DVector};
|
||||
|
||||
/// Assemble the ECSW system `C`, `b` over the given snapshots.
|
||||
fn assemble_system(
|
||||
mesh: &Mesh,
|
||||
materials: &MaterialDatabase,
|
||||
operators: &[ElementOperator],
|
||||
snapshots: &[DVector<f64>],
|
||||
modes: usize,
|
||||
) -> FeaResult<(DMatrix<f64>, DVector<f64>)> {
|
||||
let mut c = DMatrix::zeros(snapshots.len() * modes, operators.len());
|
||||
let mut b = DVector::zeros(snapshots.len() * modes);
|
||||
for (s, snapshot) in snapshots.iter().enumerate() {
|
||||
for (e, operator) in operators.iter().enumerate() {
|
||||
let reduced_force =
|
||||
operator.reduced_internal_force(materials, mesh.spatial_dimension, snapshot)?;
|
||||
for m in 0..modes {
|
||||
c[(s * modes + m, e)] = reduced_force[m];
|
||||
b[s * modes + m] += reduced_force[m];
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((c, b))
|
||||
}
|
||||
|
||||
/// The trained element sample: which elements carry the reduced internal
|
||||
/// force, and with what weights.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -48,6 +71,28 @@ pub fn train_ecsw(
|
||||
basis: &DMatrix<f64>,
|
||||
snapshots: &[DVector<f64>],
|
||||
tolerance: f64,
|
||||
) -> FeaResult<EcswModel> {
|
||||
train_ecsw_formulated(
|
||||
mesh,
|
||||
materials,
|
||||
dof_numbering,
|
||||
basis,
|
||||
snapshots,
|
||||
tolerance,
|
||||
Formulation::SmallStrain,
|
||||
)
|
||||
}
|
||||
|
||||
/// [`train_ecsw`] with an explicit internal-force formulation — must match
|
||||
/// the full-order analysis that produced the snapshots.
|
||||
pub fn train_ecsw_formulated(
|
||||
mesh: &Mesh,
|
||||
materials: &MaterialDatabase,
|
||||
dof_numbering: &AdvancedDofNumbering,
|
||||
basis: &DMatrix<f64>,
|
||||
snapshots: &[DVector<f64>],
|
||||
tolerance: f64,
|
||||
formulation: Formulation,
|
||||
) -> FeaResult<EcswModel> {
|
||||
if snapshots.is_empty() {
|
||||
return Err(FeaError::InvalidInput(
|
||||
@@ -55,22 +100,9 @@ pub fn train_ecsw(
|
||||
));
|
||||
}
|
||||
let modes = basis.ncols();
|
||||
let operators = ElementOperator::build_all(mesh, materials, dof_numbering, basis)?;
|
||||
let num_elements = operators.len();
|
||||
let operators = ElementOperator::build_all(mesh, materials, dof_numbering, basis, formulation)?;
|
||||
|
||||
let mut c = DMatrix::zeros(snapshots.len() * modes, num_elements);
|
||||
let mut b = DVector::zeros(snapshots.len() * modes);
|
||||
|
||||
for (s, snapshot) in snapshots.iter().enumerate() {
|
||||
for (e, operator) in operators.iter().enumerate() {
|
||||
let reduced_force =
|
||||
operator.reduced_internal_force(materials, mesh.spatial_dimension, snapshot)?;
|
||||
for m in 0..modes {
|
||||
c[(s * modes + m, e)] = reduced_force[m];
|
||||
b[s * modes + m] += reduced_force[m];
|
||||
}
|
||||
}
|
||||
}
|
||||
let (c, b) = assemble_system(mesh, materials, &operators, snapshots, modes)?;
|
||||
|
||||
let scale = b.norm();
|
||||
let weights = nnls(&c, &b, tolerance * scale)?;
|
||||
@@ -92,3 +124,39 @@ pub fn train_ecsw(
|
||||
training_residual,
|
||||
})
|
||||
}
|
||||
|
||||
/// Evaluate a trained model's sampled-force residual `||C w − b|| / ||b||`
|
||||
/// on an arbitrary snapshot set — the held-out generalization measurement:
|
||||
/// train on one split, call this on the other. On the training set itself
|
||||
/// it reproduces [`EcswModel::training_residual`].
|
||||
pub fn ecsw_residual(
|
||||
mesh: &Mesh,
|
||||
materials: &MaterialDatabase,
|
||||
dof_numbering: &AdvancedDofNumbering,
|
||||
basis: &DMatrix<f64>,
|
||||
snapshots: &[DVector<f64>],
|
||||
model: &EcswModel,
|
||||
formulation: Formulation,
|
||||
) -> FeaResult<f64> {
|
||||
if snapshots.is_empty() {
|
||||
return Err(FeaError::InvalidInput(
|
||||
"residual evaluation needs at least one snapshot".to_string(),
|
||||
));
|
||||
}
|
||||
let modes = basis.ncols();
|
||||
let operators = ElementOperator::build_all(mesh, materials, dof_numbering, basis, formulation)?;
|
||||
let (c, b) = assemble_system(mesh, materials, &operators, snapshots, modes)?;
|
||||
|
||||
let mut weights = DVector::zeros(operators.len());
|
||||
for &(element_id, weight) in &model.weights {
|
||||
if let Some(index) = operators.iter().position(|op| op.element_id == element_id) {
|
||||
weights[index] = weight;
|
||||
}
|
||||
}
|
||||
let scale = b.norm();
|
||||
Ok(if scale > 0.0 {
|
||||
(&c * &weights - &b).norm() / scale
|
||||
} else {
|
||||
0.0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,17 +13,20 @@
|
||||
//! 4. [`reduced::ReducedNonlinearModel`] — Newton in reduced coordinates,
|
||||
//! assembling only the sampled elements.
|
||||
//!
|
||||
//! Scope: geometrically linear, materially nonlinear (what the nonlinear
|
||||
//! analysis supports), homogeneous Dirichlet data. Lifting for
|
||||
//! inhomogeneous boundary values is not implemented and the basis is over
|
||||
//! the free DOFs only.
|
||||
//! Scope: homogeneous Dirichlet data; the basis is over the free DOFs
|
||||
//! only (lifting for inhomogeneous boundary values is not implemented).
|
||||
//! Kinematics are selected per [`reduced::Formulation`]: the original
|
||||
//! small-strain scope (geometrically linear, materially nonlinear), or
|
||||
//! total-Lagrangian Saint Venant–Kirchhoff matching
|
||||
//! `NonlinearDynamicAnalysis::with_total_lagrangian` — required when the
|
||||
//! snapshots come from a total-Lagrangian trajectory (the FSI flag).
|
||||
|
||||
pub mod ecsw;
|
||||
pub mod nnls;
|
||||
pub mod pod;
|
||||
pub mod reduced;
|
||||
|
||||
pub use ecsw::{EcswModel, train_ecsw};
|
||||
pub use ecsw::{EcswModel, ecsw_residual, train_ecsw, train_ecsw_formulated};
|
||||
pub use nnls::nnls;
|
||||
pub use pod::pod_basis;
|
||||
pub use reduced::ReducedNonlinearModel;
|
||||
pub use reduced::{Formulation, ReducedNonlinearModel};
|
||||
|
||||
@@ -4,12 +4,30 @@
|
||||
|
||||
use super::ecsw::EcswModel;
|
||||
use crate::assembly::dof_mapping::AdvancedDofNumbering;
|
||||
use crate::elements::total_lagrangian::{self, saint_venant_kirchhoff};
|
||||
use crate::elements::{ElementMatrixComputer, StandardFiniteElement};
|
||||
use crate::error::{AnalysisError, FeaError, FeaResult};
|
||||
use crate::materials::MaterialDatabase;
|
||||
use crate::mesh::{ElementId, Mesh};
|
||||
use nalgebra::{DMatrix, DVector, Vector3};
|
||||
|
||||
/// Which internal-force kinematics the reduced operators assemble.
|
||||
///
|
||||
/// Must match the full-order analysis that produced the snapshots: a
|
||||
/// basis trained on total-Lagrangian trajectories (e.g. the FSI flag,
|
||||
/// which marches `with_total_lagrangian`) sampled through small-strain
|
||||
/// operators would conserve the virtual work of the WRONG force. The
|
||||
/// small-strain variant is the original scope ("geometrically linear,
|
||||
/// materially nonlinear"); the total-Lagrangian variant mirrors
|
||||
/// `NonlinearDynamicAnalysis`'s branch exactly (Green–Lagrange strain,
|
||||
/// Saint Venant–Kirchhoff constitutive from the material's Lamé
|
||||
/// parameters, geometric tangent included).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Formulation {
|
||||
SmallStrain,
|
||||
TotalLagrangian,
|
||||
}
|
||||
|
||||
/// One element's contribution to the reduced model: its finite element, its
|
||||
/// gather map into the free-DOF vector, and its slice of the basis.
|
||||
pub(crate) struct ElementOperator {
|
||||
@@ -24,6 +42,7 @@ pub(crate) struct ElementOperator {
|
||||
/// rows for constrained DOFs.
|
||||
local_basis: DMatrix<f64>,
|
||||
material_index: crate::mesh::MaterialId,
|
||||
formulation: Formulation,
|
||||
}
|
||||
|
||||
impl ElementOperator {
|
||||
@@ -32,6 +51,7 @@ impl ElementOperator {
|
||||
materials: &MaterialDatabase,
|
||||
dof_numbering: &AdvancedDofNumbering,
|
||||
basis: &DMatrix<f64>,
|
||||
formulation: Formulation,
|
||||
) -> FeaResult<Vec<Self>> {
|
||||
let modes = basis.ncols();
|
||||
let total = dof_numbering.total_dofs;
|
||||
@@ -89,6 +109,7 @@ impl ElementOperator {
|
||||
free_positions,
|
||||
local_basis,
|
||||
material_index: element.material_id,
|
||||
formulation,
|
||||
});
|
||||
}
|
||||
Ok(operators)
|
||||
@@ -113,14 +134,30 @@ impl ElementOperator {
|
||||
let material = materials
|
||||
.get_material(self.material_index)
|
||||
.expect("checked at build time");
|
||||
let constitutive = crate::materials::reduced_constitutive(material, spatial_dim)?;
|
||||
ElementMatrixComputer::compute_internal_force_and_tangent(
|
||||
&self.finite_element,
|
||||
&self.node_coords,
|
||||
local_displacement,
|
||||
constitutive.as_ref(),
|
||||
None,
|
||||
)
|
||||
match self.formulation {
|
||||
Formulation::SmallStrain => {
|
||||
let constitutive = crate::materials::reduced_constitutive(material, spatial_dim)?;
|
||||
ElementMatrixComputer::compute_internal_force_and_tangent(
|
||||
&self.finite_element,
|
||||
&self.node_coords,
|
||||
local_displacement,
|
||||
constitutive.as_ref(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
Formulation::TotalLagrangian => {
|
||||
// Mirrors NonlinearDynamicAnalysis's total-Lagrangian branch.
|
||||
let (lambda, mu) = material.properties().lame_parameters();
|
||||
let constitutive = saint_venant_kirchhoff(lambda, mu, spatial_dim);
|
||||
total_lagrangian::internal_force_and_tangent(
|
||||
&self.finite_element,
|
||||
&self.node_coords,
|
||||
local_displacement,
|
||||
constitutive.as_ref(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `V_e' f_e(u)` for a full free-DOF displacement — used by training.
|
||||
@@ -148,14 +185,34 @@ pub struct ReducedNonlinearModel<'a> {
|
||||
}
|
||||
|
||||
impl<'a> ReducedNonlinearModel<'a> {
|
||||
/// Build a POD-Galerkin model: every element, weight 1.
|
||||
/// Build a POD-Galerkin model: every element, weight 1 (small-strain
|
||||
/// operators — the original scope).
|
||||
pub fn new(
|
||||
mesh: &'a Mesh,
|
||||
materials: &'a MaterialDatabase,
|
||||
dof_numbering: &AdvancedDofNumbering,
|
||||
basis: DMatrix<f64>,
|
||||
) -> FeaResult<Self> {
|
||||
let operators = ElementOperator::build_all(mesh, materials, dof_numbering, &basis)?;
|
||||
Self::new_formulated(
|
||||
mesh,
|
||||
materials,
|
||||
dof_numbering,
|
||||
basis,
|
||||
Formulation::SmallStrain,
|
||||
)
|
||||
}
|
||||
|
||||
/// [`Self::new`] with an explicit internal-force formulation — must
|
||||
/// match the full-order analysis that produced the training snapshots.
|
||||
pub fn new_formulated(
|
||||
mesh: &'a Mesh,
|
||||
materials: &'a MaterialDatabase,
|
||||
dof_numbering: &AdvancedDofNumbering,
|
||||
basis: DMatrix<f64>,
|
||||
formulation: Formulation,
|
||||
) -> FeaResult<Self> {
|
||||
let operators =
|
||||
ElementOperator::build_all(mesh, materials, dof_numbering, &basis, formulation)?;
|
||||
let active = (0..operators.len()).map(|e| (e, 1.0)).collect();
|
||||
Ok(Self {
|
||||
mesh,
|
||||
|
||||
Reference in New Issue
Block a user