Files
rustytorch/crates/specialized/rtx-fea/tests/fictitious_mass.rs
T
Omar SobhandClaude Fable 5.1 a2086a59de
CI / Format Check (push) Failing after 7s
CI / Build (macos-latest) (push) Waiting to run
CI / Test (macos-latest) (push) Blocked by required conditions
CI / Test (ubuntu-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (macos-latest) (push) Blocked by required conditions
CI / Python Bindings (maturin) (ubuntu-latest) (push) Blocked by required conditions
CI / WASM Build + Size Check (push) Blocked by required conditions
CI / Distributed Training Tests (push) Blocked by required conditions
CI / CI Success (push) Blocked by required conditions
CI / Clippy Check (push) Failing after 5s
CI / Build (ubuntu-latest) (push) Failing after 5s
Performance Benchmarks / Run Benchmarks (push) Failing after 6s
Documentation / Build User Guide (push) Successful in 6s
Documentation / Build API Documentation (push) Failing after 25s
CI / Build CPU-Only (Explicit) (push) Failing after 1m15s
P6-b: the fictitious added mass for the partitioned loop — rtx-fea NonlinearDynamicStepper::set_added_lumped_mass (a lumped per-DOF mass in the Newmark inertial residual and effective tangent, never the consistent mass or the rest state; zero = the plain stepper bit for bit) with its pin (compensated step reproduces the plain step to 2.5e-9, uncompensated moves it 11 %); the FSI2 overset harness carries RTX_FSI2O_FICT_MASS=α (α × ρ_f π (c/2)² spread over the wetted nodes) and adds the compensating load M_f ü_k of the previous subiterate to every structure solve (predictor and passes), printed in the header
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YJPeT6WA2e7YvAnS875AHL
2026-09-15 08:30:55 -05:00

191 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! P6-b (`docs/overset_metal_campaign.md` §5.17): the fictitious added mass
//! for the partitioned coupling loop. The stepper carries a lumped mass
//! `M_f` on chosen nodes; the coupling loop adds the load `M_f ü_k` from
//! the previous subiterate. Pin: solving `(M + M_f) ü = F + M_f ü_A` with
//! `ü_A` the plain step's own acceleration reproduces the plain step (the
//! fixed point is unchanged — the added terms cancel at convergence), while
//! the added mass WITHOUT its compensating load changes the step (the hook
//! is live). The Newmark effective matrix carries `M_f` (a step from rest
//! under a nodal load has the acceleration of a heavier body).
use nalgebra::Vector3;
use rtx_fea::analysis::{AnalysisConfig, NonlinearDynamicAnalysis};
use rtx_fea::assembly::dof_mapping::DofComponent;
use rtx_fea::boundary::dirichlet::{DirichletBC, DirichletType};
use rtx_fea::boundary::{BoundaryCondition, BoundaryConditionSet, SpatialFunction};
use rtx_fea::materials::{LinearElastic, MaterialDatabase};
use rtx_fea::mesh::{Element, ElementType, MaterialId, Mesh, Node, NodeId};
const E_MOD: f64 = 1.4e6;
const NU: f64 = 0.4;
const RHO: f64 = 1000.0;
fn quad8_rect_mesh(x0: f64, x1: f64, y0: f64, y1: f64, nx: usize, ny: usize) -> Mesh {
let mut mesh = Mesh::new(2).unwrap();
let (lx, ly) = (2 * nx + 1, 2 * ny + 1);
let mut grid = vec![vec![None; ly]; lx];
for (i, column) in grid.iter_mut().enumerate() {
for (j, slot) in column.iter_mut().enumerate() {
if i % 2 == 1 && j % 2 == 1 {
continue;
}
let x = x0 + (x1 - x0) * i as f64 / (2 * nx) as f64;
let y = y0 + (y1 - y0) * j as f64 / (2 * ny) as f64;
*slot = Some(mesh.add_node(Node::new_2d(x, y)));
}
}
for i in 0..nx {
for j in 0..ny {
let (a, b) = (2 * i, 2 * j);
let nodes = vec![
grid[a][b].unwrap(),
grid[a + 2][b].unwrap(),
grid[a + 2][b + 2].unwrap(),
grid[a][b + 2].unwrap(),
grid[a + 1][b].unwrap(),
grid[a + 2][b + 1].unwrap(),
grid[a + 1][b + 2].unwrap(),
grid[a][b + 1].unwrap(),
];
mesh.add_element(Element::new(ElementType::Quad8, nodes, MaterialId(0)).unwrap())
.unwrap();
}
}
mesh
}
fn materials() -> MaterialDatabase {
let mut db = MaterialDatabase::new();
db.add_material(
MaterialId(0),
LinearElastic::new(E_MOD, NU).with_density(RHO),
None,
);
db
}
fn clamp_left(mesh: &Mesh, x_left: f64) -> BoundaryConditionSet {
let clamped: Vec<NodeId> = mesh
.nodes
.iter()
.filter(|(_, node)| (node.position().x - x_left).abs() < 1e-12)
.map(|(&id, _)| id)
.collect();
let mut set = BoundaryConditionSet::new();
for component in [DofComponent::DisplacementX, DofComponent::DisplacementY] {
set.add_condition(BoundaryCondition::Dirichlet(DirichletBC {
nodes: clamped.clone(),
components: vec![component],
condition_type: DirichletType::Spatial(SpatialFunction(Box::new(|_| 0.0))),
time_range: None,
ramping_factor: 1.0,
gradual_enforcement: false,
}));
}
set
}
fn point_a(mesh: &Mesh, x: f64, y: f64) -> NodeId {
mesh.nodes
.iter()
.find(|(_, node)| {
(node.position().x - x).abs() < 1e-12 && (node.position().y - y).abs() < 1e-12
})
.map(|(&id, _)| id)
.expect("tracking point must be a mesh node")
}
#[test]
fn fictitious_mass_cancels_with_its_load_and_is_live_without_it() {
let mesh = quad8_rect_mesh(0.25, 0.6, 0.19, 0.21, 10, 2);
let dt = 0.005;
let a_node = point_a(&mesh, 0.6, 0.2);
let tip_force = Vector3::new(0.0, -0.5, 0.0);
let analysis = NonlinearDynamicAnalysis::new(
mesh.clone(),
materials(),
clamp_left(&mesh, 0.25),
dt,
1,
AnalysisConfig::default(),
)
.with_total_lagrangian();
// A: the plain step from a moving state (three plain steps in, so the
// acceleration is not the rest one).
let mut plain = analysis.stepper().unwrap();
plain.set_nodal_forces(&[(a_node, tip_force)]);
let mut state = plain.rest_state().unwrap();
for _ in 0..3 {
state = plain.step(&state).unwrap().0;
}
let (step_a, _) = plain.step(&state).unwrap();
let a_dofs = plain.node_dofs(a_node);
// The wetted nodes of this flag: everything not clamped. M_f on all of
// them, the size of the flag's own mass per node (a hard case for the
// cancellation: the added term is O(1) of the inertia).
let wetted: Vec<NodeId> = mesh
.nodes
.iter()
.filter(|(_, n)| (n.position().x - 0.25).abs() > 1e-12)
.map(|(&id, _)| id)
.collect();
let m_f = 0.5; // kg per node (the flag's 0.02 × 0.35 × 1000 = 7 kg over ~60 nodes)
let entries: Vec<(NodeId, f64)> = wetted.iter().map(|&n| (n, m_f)).collect();
// B: the added mass with the compensating load M_f ü_A (ü_A = A's own
// acceleration at each wetted node): the same step to solver tolerance.
let mut fict = analysis.stepper().unwrap();
fict.set_added_lumped_mass(&entries);
let mut loads = vec![(a_node, tip_force)];
for &n in &wetted {
let d = fict.node_dofs(n);
loads.push((
n,
Vector3::new(
m_f * step_a.acceleration[d[0]],
m_f * step_a.acceleration[d[1]],
0.0,
),
));
}
fict.set_nodal_forces(&loads);
let (step_b, _) = fict.step(&state).unwrap();
let uy_a = step_a.displacement[a_dofs[1]];
let uy_b = step_b.displacement[a_dofs[1]];
let du =
(step_a.displacement.clone() - &step_b.displacement).norm() / step_a.displacement.norm();
println!(
" cancellation: tip uy A {uy_a:.6e} vs B {uy_b:.6e}; relative displacement difference {du:.2e}"
);
assert!(
du < 1e-6,
"the compensated added mass changed the step: {du:.2e}"
);
// C: the added mass WITHOUT the compensating load: a heavier body, a
// visibly different step (the hook is live in the effective matrix).
let mut heavy = analysis.stepper().unwrap();
heavy.set_added_lumped_mass(&entries);
heavy.set_nodal_forces(&[(a_node, tip_force)]);
let (step_c, _) = heavy.step(&state).unwrap();
let dc =
(step_a.displacement.clone() - &step_c.displacement).norm() / step_a.displacement.norm();
println!(" uncompensated: relative displacement difference {dc:.2e}");
assert!(
dc > 1e-3,
"the added mass alone did not change the step: {dc:.2e}"
);
// D: zero added mass is the plain stepper bit for bit.
let mut zero = analysis.stepper().unwrap();
zero.set_added_lumped_mass(&wetted.iter().map(|&n| (n, 0.0)).collect::<Vec<_>>());
zero.set_nodal_forces(&[(a_node, tip_force)]);
let (step_d, _) = zero.step(&state).unwrap();
assert_eq!(
step_a.displacement, step_d.displacement,
"zero added mass is not bit-identical"
);
}