R8-g: parallel scatter + trailing updates + tree solves; force-only TL kernel for modified Newton

- the tangent scatter runs per CSR entry over a transposed contribution
  map, in the serial scatter's order (same bits), on rayon;
- large fronts' trailing gemms run per column block on rayon;
- forward solve multifrontal, backward top-down, subtrees on rayon;
- total_lagrangian::internal_force: the force of internal_force_and_tangent
  alone (bit-identical, tested), used by modified-Newton iterations that
  reuse the factor; the tangent is evaluated only when a refresh is due.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-09-25 22:52:23 -05:00
co-authored by Claude Opus 5.5
parent 860f5bb37f
commit d50a0e28d2
5 changed files with 459 additions and 79 deletions
@@ -12,7 +12,10 @@
//! (12×2×2), 40 CSM3 steps.
//! 4. `modified_newton_reuses_the_factor` — `reuse = 4`: fewer
//! factorisations than solves, same march to the Newton tolerance.
//! 5. `tangent_knob_parses` — the `RTX_FEA_TANGENT` values.
//! 5. `force_only_kernel_is_bit_identical` — the TL force-only kernel
//! (modified Newton's reused iterations) = the pair's force, bit for
//! bit, on Hex20 and Quad8.
//! 6. `tangent_knob_parses` — the solver enum's defaults.
//!
//! Instruments (`#[ignore]`, env-driven, write under `R8G_OUT`):
//!
@@ -306,6 +309,34 @@ fn modified_newton_reuses_the_factor() {
assert!(r < 1e-4, "modified Newton drifts: {r:.3e}");
}
#[test]
fn force_only_kernel_is_bit_identical() {
use rtx_fea::elements::total_lagrangian::{
internal_force, internal_force_and_tangent, saint_venant_kirchhoff,
};
let flag = Flag3d::build(Flag3dSpec::turek_hron(0.1, -0.05, 3, 1, 1)).unwrap();
let quad = quad8_flag(3, 1);
for mesh in [&flag.mesh, &quad] {
let dim = mesh.spatial_dimension;
let constitutive = saint_venant_kirchhoff(8.0e5, 5.0e5, dim);
for (e, element) in mesh.elements.values().enumerate() {
let coords: Vec<Vector3<f64>> = element
.nodes
.iter()
.map(|id| mesh.get_node(*id).unwrap().position())
.collect();
let fe = StandardFiniteElement::new(element.element_type, coords.clone());
let n = coords.len() * dim;
let u = DVector::from_fn(n, |i, _| 1e-3 * ((i + 7 * e) as f64 * 0.73).sin());
let (f_pair, _) =
internal_force_and_tangent(&fe, &coords, &u, constitutive.as_ref(), None).unwrap();
let f_only = internal_force(&fe, &coords, &u, constitutive.as_ref(), None).unwrap();
assert!(f_pair.norm() > 0.0);
assert_eq!(f_pair, f_only, "force-only kernel differs");
}
}
}
#[test]
fn tangent_knob_parses() {
// Only the parser (the knob is read when a stepper is built).
@@ -527,12 +558,14 @@ fn r8g_g2_cost() {
}
let breakdown = match (before, stepper.tangent_stats()) {
(Some(b), Some(a)) => format!(
" | assemblies {} ({:.4} s each), factorisations {} ({:.4} s each incl. \
scatter), solves {} ({:.4} s each), fallbacks {}, nnz(L) {}",
" | assemblies {} ({:.4} s each), factorisations {} (scatter {:.4} + \
numeric {:.4} s each), solves {} ({:.4} s each), fallbacks {}, nnz(L) {}",
a.assemblies - b.assemblies,
(a.assembly_seconds - b.assembly_seconds)
/ (a.assemblies - b.assemblies).max(1) as f64,
a.factorizations - b.factorizations,
(a.scatter_seconds - b.scatter_seconds)
/ (a.factorizations - b.factorizations).max(1) as f64,
(a.factor_seconds - b.factor_seconds)
/ (a.factorizations - b.factorizations).max(1) as f64,
a.solves - b.solves,