rtx-fea: reduced Newmark (mor::dynamic) + the phase-4a offline replay — the ≥10x gate is REFUTED by measurement at the validated resolution
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 dynamic layer over ReducedNonlinearModel: reduced consistent mass V'MV (full element sum, never ECSW-sampled — ECSW weights are trained on internal-force virtual work and would conserve the wrong inertia), reduced_force_and_jacobian exposed (solve() refactored onto it), and ReducedNewmark mirroring NonlinearDynamicStepper::newmark_newton in reduced coordinates (same predictor, residual, tangent shape; no rescue ladder by design — a reduced Newton death is a finding). TDD (tests/reduced_newmark.rs): identity-basis march reproduces the full stepper to 2.4e-14 over 15 steps (both Newton loops tightened to 1e-10 so only solver rounding separates them); rigid-translation reduced mass = rho*A to 1e-9; a 6-mode POD basis tracks its training trajectory at 4.2e-4 rms against a 1.0e-4 projection floor. Phase 4a (fsi3_ecsw_offline.rs, fsi3_reduced_newmark_replay, env-gated): reduced Newmark replay of the harvested FSI3 trajectory at record cadence (dt_rec = 5x march dt), driven by the recorded end-of-step loads. Measured, m=12/20: - COST (dt-independent, the verdict): 3,068/3,580 us/step at 4.6/5.0 Newton iters — 2.0-2.3x the banded full-order structural step (7,200 us/pass, bandedlu_fsi3_ny62_t85). The >=10x gate needs <=720 us/step; one reduced eval alone costs ~640 us because phase 2 refuted hyperreduction (every eval loops all 70 elements). The gate arithmetic is closed: reduced Newton needs >=2 evals, capping the ROM at ~5x. THE CAMPAIGN GATE (pinned cycle bands at >=10x structural speedup) CANNOT BE MET at the validated resolution. - TRACKING at record cadence diverges in the release transient (dies t=4.35-4.45) — and the RTX_REPLAY_IDENTITY control dies EARLIER (t=4.13) in the exact subspace: the death is the 5x-coarse integration + aliased loads, NOT the reduction. The record-cadence replay cannot judge subspace dynamics; the projection floor (1.1e-3 at m=12) remains the honest subspace statement. Campaign verdict to be recorded in omni-cortex in the pre-registered words. 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
10c779e96e
commit
0b4f306ed1
@@ -0,0 +1,152 @@
|
||||
//! Reduced Newmark: the dynamic layer over [`ReducedNonlinearModel`] —
|
||||
//! phase 4 of the ECSW×FSI campaign.
|
||||
//!
|
||||
//! Mirrors `NonlinearDynamicStepper::newmark_newton` in reduced
|
||||
//! coordinates: Newmark's displacement predictor, Newton on the
|
||||
//! end-of-step reduced displacement with the tangent
|
||||
//! `V'KV + V'MV/(β Δt²)`, velocities and accelerations updated per
|
||||
//! Newmark. The reduced mass is projected once at construction (it is
|
||||
//! constant); the m×m tangent is factorized dense per iteration — at
|
||||
//! m = 12–20 that cost is noise next to the element-loop assembly.
|
||||
//!
|
||||
//! Deliberately NOT carried over from the full stepper: the rescue
|
||||
//! ladder (line search, substepping). The reduced model's first duty is
|
||||
//! the offline replay measurement; if reduced plain Newton dies at a
|
||||
//! load reversal, that is a phase-4 finding to record, not to paper
|
||||
//! over silently.
|
||||
|
||||
use super::reduced::ReducedNonlinearModel;
|
||||
use crate::analysis::ConvergenceCriteria;
|
||||
use crate::error::{AnalysisError, FeaError, FeaResult};
|
||||
use nalgebra::{DMatrix, DVector};
|
||||
|
||||
/// Reduced-coordinate dynamic state (`q`, `q̇`, `q̈`), each of length
|
||||
/// `modes`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReducedState {
|
||||
pub q: DVector<f64>,
|
||||
pub q_dot: DVector<f64>,
|
||||
pub q_ddot: DVector<f64>,
|
||||
}
|
||||
|
||||
/// Newmark time stepping in the reduced coordinates of a
|
||||
/// [`ReducedNonlinearModel`].
|
||||
pub struct ReducedNewmark<'m, 'a> {
|
||||
model: &'m ReducedNonlinearModel<'a>,
|
||||
/// `V' M V`, full element sum (never ECSW-sampled).
|
||||
mass: DMatrix<f64>,
|
||||
dt: f64,
|
||||
gamma: f64,
|
||||
beta: f64,
|
||||
criteria: ConvergenceCriteria,
|
||||
}
|
||||
|
||||
impl<'m, 'a> ReducedNewmark<'m, 'a> {
|
||||
/// Average-acceleration Newmark (γ = 1/2, β = 1/4) over `model`,
|
||||
/// with the reduced mass projected here once.
|
||||
pub fn new(model: &'m ReducedNonlinearModel<'a>, dt: f64) -> FeaResult<Self> {
|
||||
Ok(Self {
|
||||
model,
|
||||
mass: model.reduced_mass()?,
|
||||
dt,
|
||||
gamma: 0.5,
|
||||
beta: 0.25,
|
||||
criteria: ConvergenceCriteria::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Newmark parameters (default γ = 1/2, β = 1/4).
|
||||
#[must_use]
|
||||
pub fn with_newmark_parameters(mut self, gamma: f64, beta: f64) -> Self {
|
||||
self.gamma = gamma;
|
||||
self.beta = beta;
|
||||
self
|
||||
}
|
||||
|
||||
/// Convergence criteria for the per-step Newton loop (default:
|
||||
/// [`ConvergenceCriteria::default`], as on the full analysis).
|
||||
#[must_use]
|
||||
pub fn with_convergence_criteria(mut self, criteria: ConvergenceCriteria) -> Self {
|
||||
self.criteria = criteria;
|
||||
self
|
||||
}
|
||||
|
||||
/// The state at rest under `external_force_free`: `q = q̇ = 0`, the
|
||||
/// acceleration consistent with `M_r q̈ = V'(F_ext) − f_int_r(0)` —
|
||||
/// the reduced mirror of the full stepper's `rest_state`.
|
||||
pub fn rest_state(&self, external_force_free: &DVector<f64>) -> FeaResult<ReducedState> {
|
||||
let modes = self.model.modes();
|
||||
let q = DVector::zeros(modes);
|
||||
let (internal, _) = self.model.reduced_force_and_jacobian(&q)?;
|
||||
let residual = self.model.reduce_vector(external_force_free) - internal;
|
||||
let q_ddot = self
|
||||
.mass
|
||||
.clone()
|
||||
.lu()
|
||||
.solve(&residual)
|
||||
.ok_or_else(|| FeaError::InvalidInput("singular reduced mass".to_string()))?;
|
||||
Ok(ReducedState {
|
||||
q,
|
||||
q_dot: DVector::zeros(modes),
|
||||
q_ddot,
|
||||
})
|
||||
}
|
||||
|
||||
/// One Newmark step of `dt` from `state` under the end-of-step load
|
||||
/// `external_force_free` (free-DOF space; projected here). Returns
|
||||
/// the end-of-step state and the Newton iteration count. Pure
|
||||
/// function of `(state, load)` — commits nothing.
|
||||
pub fn step(
|
||||
&self,
|
||||
state: &ReducedState,
|
||||
external_force_free: &DVector<f64>,
|
||||
) -> FeaResult<(ReducedState, usize)> {
|
||||
let dt = self.dt;
|
||||
let (gamma, beta) = (self.gamma, self.beta);
|
||||
let inv_beta_dt2 = 1.0 / (beta * dt * dt);
|
||||
|
||||
let reduced_external = self.model.reduce_vector(external_force_free);
|
||||
let force_scale = reduced_external.norm().max(1.0);
|
||||
|
||||
let q_pred = &state.q + dt * &state.q_dot + dt * dt * (0.5 - beta) * &state.q_ddot;
|
||||
|
||||
let mut q_iter = q_pred.clone();
|
||||
let mut converged = false;
|
||||
let mut iterations = 0usize;
|
||||
for _ in 0..self.criteria.max_iterations {
|
||||
let a_new = inv_beta_dt2 * (&q_iter - &q_pred);
|
||||
let (internal, jacobian) = self.model.reduced_force_and_jacobian(&q_iter)?;
|
||||
let residual = &reduced_external - internal - &self.mass * &a_new;
|
||||
if residual.norm() < self.criteria.force_tolerance * force_scale {
|
||||
converged = true;
|
||||
break;
|
||||
}
|
||||
iterations += 1;
|
||||
let tangent = jacobian + inv_beta_dt2 * &self.mass;
|
||||
let delta = tangent
|
||||
.lu()
|
||||
.solve(&residual)
|
||||
.ok_or_else(|| FeaError::InvalidInput("singular reduced tangent".to_string()))?;
|
||||
let delta_norm = delta.norm();
|
||||
q_iter += delta;
|
||||
if delta_norm < self.criteria.displacement_tolerance * q_iter.norm().max(1.0) {
|
||||
converged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !converged {
|
||||
return Err(AnalysisError::ConvergenceFailed { iterations }.into());
|
||||
}
|
||||
|
||||
let q_ddot_new = inv_beta_dt2 * (&q_iter - &q_pred);
|
||||
let q_dot_new = &state.q_dot + dt * ((1.0 - gamma) * &state.q_ddot + gamma * &q_ddot_new);
|
||||
Ok((
|
||||
ReducedState {
|
||||
q: q_iter,
|
||||
q_dot: q_dot_new,
|
||||
q_ddot: q_ddot_new,
|
||||
},
|
||||
iterations,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,13 @@
|
||||
//! `NonlinearDynamicAnalysis::with_total_lagrangian` — required when the
|
||||
//! snapshots come from a total-Lagrangian trajectory (the FSI flag).
|
||||
|
||||
pub mod dynamic;
|
||||
pub mod ecsw;
|
||||
pub mod nnls;
|
||||
pub mod pod;
|
||||
pub mod reduced;
|
||||
|
||||
pub use dynamic::{ReducedNewmark, ReducedState};
|
||||
pub use ecsw::{EcswModel, ecsw_residual, train_ecsw, train_ecsw_formulated};
|
||||
pub use nnls::nnls;
|
||||
pub use pod::pod_basis;
|
||||
|
||||
@@ -160,6 +160,37 @@ impl ElementOperator {
|
||||
}
|
||||
}
|
||||
|
||||
/// `V_e' M_e V_e` — this element's contribution to the reduced
|
||||
/// consistent mass. The scalar consistent mass is expanded to vector
|
||||
/// DOFs exactly as `NonlinearDynamicStepper` builds `mass_free`.
|
||||
pub(crate) fn reduced_mass(
|
||||
&self,
|
||||
materials: &MaterialDatabase,
|
||||
spatial_dim: usize,
|
||||
) -> FeaResult<DMatrix<f64>> {
|
||||
let material = materials
|
||||
.get_material(self.material_index)
|
||||
.expect("checked at build time");
|
||||
let density = material.properties().density;
|
||||
let scalar = ElementMatrixComputer::compute_consistent_mass_matrix(
|
||||
&self.finite_element,
|
||||
&self.node_coords,
|
||||
density,
|
||||
None,
|
||||
)?;
|
||||
let nodes = self.node_coords.len();
|
||||
let mut mass = DMatrix::zeros(nodes * spatial_dim, nodes * spatial_dim);
|
||||
for a in 0..nodes {
|
||||
for b in 0..nodes {
|
||||
let m = scalar.matrix[(a, b)];
|
||||
for d in 0..spatial_dim {
|
||||
mass[(a * spatial_dim + d, b * spatial_dim + d)] = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(self.local_basis.transpose() * mass * &self.local_basis)
|
||||
}
|
||||
|
||||
/// `V_e' f_e(u)` for a full free-DOF displacement — used by training.
|
||||
pub(crate) fn reduced_internal_force(
|
||||
&self,
|
||||
@@ -245,6 +276,58 @@ impl<'a> ReducedNonlinearModel<'a> {
|
||||
self.active.len()
|
||||
}
|
||||
|
||||
/// Number of modes (reduced coordinates).
|
||||
pub fn modes(&self) -> usize {
|
||||
self.basis.ncols()
|
||||
}
|
||||
|
||||
/// `V' v` — project a free-DOF vector into reduced coordinates.
|
||||
pub fn reduce_vector(&self, free_vector: &DVector<f64>) -> DVector<f64> {
|
||||
self.basis.transpose() * free_vector
|
||||
}
|
||||
|
||||
/// `V q` — expand reduced coordinates to the free-DOF space.
|
||||
pub fn expand(&self, q: &DVector<f64>) -> DVector<f64> {
|
||||
&self.basis * q
|
||||
}
|
||||
|
||||
/// The reduced consistent mass `V' M V`, summed over EVERY element —
|
||||
/// never the ECSW sample: ECSW weights are trained on internal-force
|
||||
/// virtual work only, and reweighting the mass with them would
|
||||
/// conserve the wrong inertia.
|
||||
pub fn reduced_mass(&self) -> FeaResult<DMatrix<f64>> {
|
||||
let modes = self.basis.ncols();
|
||||
let spatial_dim = self.mesh.spatial_dimension;
|
||||
let mut mass = DMatrix::zeros(modes, modes);
|
||||
for operator in &self.operators {
|
||||
mass += operator.reduced_mass(self.materials, spatial_dim)?;
|
||||
}
|
||||
Ok(mass)
|
||||
}
|
||||
|
||||
/// The reduced internal force `Σ w_e V_e' f_e(V q)` and tangent
|
||||
/// `Σ w_e V_e' K_e V_e` over the active element set, at reduced
|
||||
/// coordinates `q` — one Newton iteration's assembly, exposed for
|
||||
/// the dynamic (Newmark) driver.
|
||||
pub fn reduced_force_and_jacobian(
|
||||
&self,
|
||||
q: &DVector<f64>,
|
||||
) -> FeaResult<(DVector<f64>, DMatrix<f64>)> {
|
||||
let modes = self.basis.ncols();
|
||||
let spatial_dim = self.mesh.spatial_dimension;
|
||||
let free_displacement = &self.basis * q;
|
||||
let mut force = DVector::zeros(modes);
|
||||
let mut jacobian = DMatrix::zeros(modes, modes);
|
||||
for &(index, weight) in &self.active {
|
||||
let operator = &self.operators[index];
|
||||
let local = operator.gather(&free_displacement);
|
||||
let (f, k) = operator.force_and_tangent(self.materials, &local, spatial_dim)?;
|
||||
force += operator.local_basis.transpose() * f * weight;
|
||||
jacobian += operator.local_basis.transpose() * k * &operator.local_basis * weight;
|
||||
}
|
||||
Ok((force, jacobian))
|
||||
}
|
||||
|
||||
/// Assemble the weighted reduced internal force `Σ w_e V_e' f_e(u)`
|
||||
/// over the active element set at a full free-DOF displacement — the
|
||||
/// per-Newton-iteration cost ECSW reduces, exposed for wall-clock
|
||||
@@ -277,25 +360,13 @@ impl<'a> ReducedNonlinearModel<'a> {
|
||||
max_iterations: usize,
|
||||
) -> FeaResult<DVector<f64>> {
|
||||
let modes = self.basis.ncols();
|
||||
let spatial_dim = self.mesh.spatial_dimension;
|
||||
let reduced_external = self.basis.transpose() * external_force_free;
|
||||
let scale = reduced_external.norm().max(1.0);
|
||||
|
||||
let mut q = DVector::zeros(modes);
|
||||
for _iteration in 0..max_iterations {
|
||||
let free_displacement = &self.basis * &q;
|
||||
|
||||
let mut residual = reduced_external.clone();
|
||||
let mut jacobian = DMatrix::zeros(modes, modes);
|
||||
for &(index, weight) in &self.active {
|
||||
let operator = &self.operators[index];
|
||||
let local = operator.gather(&free_displacement);
|
||||
let (force, tangent) =
|
||||
operator.force_and_tangent(self.materials, &local, spatial_dim)?;
|
||||
residual -= operator.local_basis.transpose() * force * weight;
|
||||
jacobian +=
|
||||
operator.local_basis.transpose() * tangent * &operator.local_basis * weight;
|
||||
}
|
||||
let (internal, jacobian) = self.reduced_force_and_jacobian(&q)?;
|
||||
let residual = &reduced_external - internal;
|
||||
|
||||
if residual.norm() < force_tolerance * scale {
|
||||
return Ok(&self.basis * &q);
|
||||
|
||||
Reference in New Issue
Block a user