R8-g: sparse supernodal LDLt for the Newton tangent (default off)
rtx_fea::solvers::SparseLdlt: nested-dissection ordering, etree,
fundamental supernodes, multifrontal numeric factorisation (blocked
LDLt, matrixmultiply gemm, rayon over subtrees, bit-deterministic at
any thread count), symbolic analysis reused while the pattern holds.
NonlinearDynamicStepper: TangentSolver::{BandedLu (default, unchanged
float for float), SparseLdlt { reuse }} via with_tangent_solver or
RTX_FEA_TANGENT=sparse[:K]; fixed-pattern CSR assembled from parallel
element evaluations (forces summed in the banded path's order);
optional modified Newton (factor reuse). The per-element kernel is
factored out of assemble() unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
171da41ed1
commit
860f5bb37f
@@ -0,0 +1,559 @@
|
||||
//! R8-g: the sparse Newton-tangent path of the nonlinear Newmark stepper
|
||||
//! ([`TangentSolver::SparseLdlt`]) against the default banded LU.
|
||||
//!
|
||||
//! Suite (fast, run by default):
|
||||
//!
|
||||
//! 1. `sparse_matches_banded_csm3_2d` — the 2-D 35×2 Quad8 CSM3 march
|
||||
//! (the FSI2 harness's flag), 60 steps: every step's displacement
|
||||
//! within 1e-10 of the banded path's (relative to the peak).
|
||||
//! 2. `sparse_matches_banded_csm1_3d_plane_strain` — CSM1 static on the
|
||||
//! 3-D 35×2×1 plane-strain flag.
|
||||
//! 3. `sparse_matches_banded_csm3_3d_free` — a free-edge 3-D flag
|
||||
//! (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.
|
||||
//!
|
||||
//! Instruments (`#[ignore]`, env-driven, write under `R8G_OUT`):
|
||||
//!
|
||||
//! * `r8g_g1_equivalence` — CSM1 static and a CSM3 march per
|
||||
//! configuration with both solvers side by side; per-step CSV.
|
||||
//! * `r8g_g2_cost` — wall time per Newton iteration and per step, per
|
||||
//! solver mode and configuration, with the sparse path's breakdown.
|
||||
|
||||
use std::io::Write as _;
|
||||
use std::time::Instant;
|
||||
|
||||
use nalgebra::{DVector, Vector3};
|
||||
use rtx_fea::analysis::flag3d::{Flag3d, Flag3dSpec, LateralFaces};
|
||||
use rtx_fea::analysis::{
|
||||
ConvergenceCriteria, DynamicState, NonlinearDynamicAnalysis, TangentSolver,
|
||||
};
|
||||
use rtx_fea::assembly::dof_mapping::DofComponent;
|
||||
use rtx_fea::boundary::dirichlet::{DirichletBC, DirichletType};
|
||||
use rtx_fea::boundary::{BoundaryCondition, BoundaryConditionSet, SpatialFunction};
|
||||
use rtx_fea::elements::{ElementMatrixComputer, StandardFiniteElement};
|
||||
use rtx_fea::mesh::{Element, ElementType, MaterialId, Mesh, Node, NodeId};
|
||||
|
||||
const E_MOD: f64 = 1.4e6;
|
||||
const NU: f64 = 0.4;
|
||||
const RHO_CSM: f64 = 1000.0;
|
||||
const G: f64 = 2.0;
|
||||
|
||||
fn env_str(name: &str, default: &str) -> String {
|
||||
std::env::var(name).unwrap_or_else(|_| default.to_string())
|
||||
}
|
||||
|
||||
fn env_num(name: &str, default: f64) -> f64 {
|
||||
std::env::var(name)
|
||||
.map(|v| v.parse().expect(name))
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
/// The 2-D flag, as the FSI2 harness builds it (copied from
|
||||
/// `flag3d_structure.rs`).
|
||||
fn quad8_flag(nx: usize, ny: usize) -> Mesh {
|
||||
let (x0, x1, y0, y1) = (0.25, 0.6, 0.19, 0.21);
|
||||
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 clamp_2d(mesh: &Mesh) -> BoundaryConditionSet {
|
||||
let clamped: Vec<NodeId> = mesh
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|(_, node)| (node.position().x - 0.25).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
|
||||
}
|
||||
|
||||
/// Consistent gravity nodal forces `∫ N_a ρ g dV`.
|
||||
fn gravity_forces(mesh: &Mesh, rho: f64, g: f64) -> Vec<(NodeId, Vector3<f64>)> {
|
||||
let dim = mesh.spatial_dimension;
|
||||
let mut acc: std::collections::BTreeMap<NodeId, Vector3<f64>> = Default::default();
|
||||
for element in mesh.elements.values() {
|
||||
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 f = ElementMatrixComputer::compute_body_force_vector(
|
||||
&fe,
|
||||
&coords,
|
||||
&|_| Vector3::new(0.0, -rho * g, 0.0),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
for (a, id) in element.nodes.iter().enumerate() {
|
||||
let e = acc.entry(*id).or_insert_with(Vector3::zeros);
|
||||
for c in 0..dim {
|
||||
e[c] += f[a * dim + c];
|
||||
}
|
||||
}
|
||||
}
|
||||
acc.into_iter().collect()
|
||||
}
|
||||
|
||||
fn static_criteria() -> ConvergenceCriteria {
|
||||
ConvergenceCriteria {
|
||||
force_tolerance: 1e-12,
|
||||
displacement_tolerance: 1e-14,
|
||||
max_iterations: 60,
|
||||
..ConvergenceCriteria::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The CSM3 analysis (gravity from rest) on the 2-D 35×2 flag.
|
||||
fn csm3_2d(dt: f64, solver: TangentSolver) -> NonlinearDynamicAnalysis {
|
||||
let mesh = quad8_flag(35, 2);
|
||||
let mut analysis = NonlinearDynamicAnalysis::new(
|
||||
mesh.clone(),
|
||||
Flag3d::materials(E_MOD, NU, RHO_CSM),
|
||||
clamp_2d(&mesh),
|
||||
dt,
|
||||
1,
|
||||
Default::default(),
|
||||
)
|
||||
.with_total_lagrangian()
|
||||
.with_tangent_solver(solver);
|
||||
analysis.set_body_force(|_| Vector3::new(0.0, -RHO_CSM * G, 0.0));
|
||||
analysis
|
||||
}
|
||||
|
||||
/// The CSM3 analysis on a 3-D flag (R8-b's instrument settings).
|
||||
fn csm3_3d(
|
||||
flag: &Flag3d,
|
||||
lateral: LateralFaces,
|
||||
dt: f64,
|
||||
solver: TangentSolver,
|
||||
) -> NonlinearDynamicAnalysis {
|
||||
let mut analysis = flag
|
||||
.dynamic_analysis(E_MOD, NU, RHO_CSM, lateral, dt, 1, 0.5)
|
||||
.with_tangent_solver(solver);
|
||||
analysis.set_body_force(|_| Vector3::new(0.0, -RHO_CSM * G, 0.0));
|
||||
analysis
|
||||
}
|
||||
|
||||
/// `max_i |a_i − b_i| / max_i |b_i|`.
|
||||
fn rel_max(a: &DVector<f64>, b: &DVector<f64>) -> f64 {
|
||||
let scale = b.amax().max(1e-300);
|
||||
(a - b).amax() / scale
|
||||
}
|
||||
|
||||
/// March both analyses `steps` steps side by side; per step the
|
||||
/// relative displacement difference. Returns (max rel, per-step rows).
|
||||
fn march_pair(
|
||||
banded: &NonlinearDynamicAnalysis,
|
||||
sparse: &NonlinearDynamicAnalysis,
|
||||
steps: usize,
|
||||
) -> (f64, Vec<(f64, f64, usize, usize)>) {
|
||||
let mut sb = banded.stepper().unwrap();
|
||||
let mut ss = sparse.stepper().unwrap();
|
||||
let mut stb = sb.rest_state().unwrap();
|
||||
let mut sts = ss.rest_state().unwrap();
|
||||
let mut worst: f64 = rel_max(&sts.acceleration, &stb.acceleration);
|
||||
let mut rows = Vec::new();
|
||||
for _ in 0..steps {
|
||||
let t0 = Instant::now();
|
||||
let (nb, ib) = sb.step(&stb).unwrap();
|
||||
let tb = t0.elapsed().as_secs_f64();
|
||||
let t1 = Instant::now();
|
||||
let (ns, is) = ss.step(&sts).unwrap();
|
||||
let ts = t1.elapsed().as_secs_f64();
|
||||
stb = nb;
|
||||
sts = ns;
|
||||
let r = rel_max(&sts.displacement, &stb.displacement);
|
||||
worst = worst.max(r);
|
||||
rows.push((r, tb / ts.max(1e-12), ib, is));
|
||||
}
|
||||
(worst, rows)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sparse_matches_banded_csm3_2d() {
|
||||
let dt = 0.005;
|
||||
let (worst, rows) = march_pair(
|
||||
&csm3_2d(dt, TangentSolver::BandedLu),
|
||||
&csm3_2d(dt, TangentSolver::SPARSE),
|
||||
60,
|
||||
);
|
||||
let same_newton = rows.iter().all(|r| r.2 == r.3);
|
||||
println!("CSM3 2-D 35x2, 60 steps: max rel |Δu| {worst:.3e}, same Newton counts {same_newton}");
|
||||
assert!(worst < 1e-10, "sparse departs from banded: {worst:.3e}");
|
||||
assert!(same_newton);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sparse_matches_banded_csm1_3d_plane_strain() {
|
||||
let flag = Flag3d::build(Flag3dSpec::turek_hron(0.05, 0.0, 35, 2, 1)).unwrap();
|
||||
let forces = gravity_forces(&flag.mesh, RHO_CSM, G);
|
||||
let solve = |solver: TangentSolver| {
|
||||
let analysis = flag
|
||||
.dynamic_analysis(E_MOD, NU, RHO_CSM, LateralFaces::PlaneStrain, 1e4, 1, 0.5)
|
||||
.with_convergence_criteria(static_criteria())
|
||||
.with_tangent_solver(solver);
|
||||
let mut stepper = analysis.stepper().unwrap();
|
||||
let n = stepper.rest_state().unwrap().displacement.len();
|
||||
let mut u = DVector::zeros(n);
|
||||
for s in 1..=5 {
|
||||
let scale = s as f64 / 5.0;
|
||||
let scaled: Vec<_> = forces.iter().map(|(id, f)| (*id, f * scale)).collect();
|
||||
stepper.set_nodal_forces(&scaled);
|
||||
let state = DynamicState {
|
||||
displacement: u.clone(),
|
||||
velocity: DVector::zeros(n),
|
||||
acceleration: DVector::zeros(n),
|
||||
};
|
||||
u = stepper.step(&state).unwrap().0.displacement;
|
||||
}
|
||||
let d = stepper.node_dofs(flag.point_a());
|
||||
(u.clone(), u[d[1]])
|
||||
};
|
||||
let (ub, ay_b) = solve(TangentSolver::BandedLu);
|
||||
let (us, ay_s) = solve(TangentSolver::SPARSE);
|
||||
let r = rel_max(&us, &ub);
|
||||
println!(
|
||||
"CSM1 3-D 35x2x1 ps: uy(A) banded {ay_b:.12e} sparse {ay_s:.12e}, max rel |Δu| {r:.3e}"
|
||||
);
|
||||
assert!(r < 1e-10, "CSM1 departs: {r:.3e}");
|
||||
assert!((ay_b + 65.1406e-3).abs() < 1e-6, "CSM1 uy(A) moved: {ay_b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sparse_matches_banded_csm3_3d_free() {
|
||||
let flag = Flag3d::build(Flag3dSpec::turek_hron(0.1, -0.05, 12, 2, 2)).unwrap();
|
||||
let dt = 0.005;
|
||||
let (worst, _) = march_pair(
|
||||
&csm3_3d(&flag, LateralFaces::Free, dt, TangentSolver::BandedLu),
|
||||
&csm3_3d(&flag, LateralFaces::Free, dt, TangentSolver::SPARSE),
|
||||
40,
|
||||
);
|
||||
println!("CSM3 3-D 12x2x2 free, 40 steps: max rel |Δu| {worst:.3e}");
|
||||
assert!(worst < 1e-10, "sparse departs from banded: {worst:.3e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modified_newton_reuses_the_factor() {
|
||||
let flag = Flag3d::build(Flag3dSpec::turek_hron(0.1, -0.05, 12, 2, 2)).unwrap();
|
||||
let dt = 0.005;
|
||||
let full = csm3_3d(&flag, LateralFaces::Free, dt, TangentSolver::SPARSE);
|
||||
let modified = csm3_3d(
|
||||
&flag,
|
||||
LateralFaces::Free,
|
||||
dt,
|
||||
TangentSolver::SparseLdlt { reuse: 4 },
|
||||
);
|
||||
let mut sf = full.stepper().unwrap();
|
||||
let mut sm = modified.stepper().unwrap();
|
||||
let mut stf = sf.rest_state().unwrap();
|
||||
let mut stm = sm.rest_state().unwrap();
|
||||
for _ in 0..40 {
|
||||
stf = sf.step(&stf).unwrap().0;
|
||||
stm = sm.step(&stm).unwrap().0;
|
||||
}
|
||||
let r = rel_max(&stm.displacement, &stf.displacement);
|
||||
let stats_f = sf.tangent_stats().unwrap();
|
||||
let stats_m = sm.tangent_stats().unwrap();
|
||||
println!(
|
||||
"modified Newton (reuse 4) vs full after 40 steps: rel {r:.3e}; full {stats_f:?}; modified {stats_m:?}"
|
||||
);
|
||||
assert_eq!(stats_f.factorizations, stats_f.solves);
|
||||
assert!(stats_m.factorizations < stats_m.solves);
|
||||
assert_eq!(stats_m.analyses, 1);
|
||||
// Newton's own displacement tolerance is 1e-6 (relative).
|
||||
assert!(r < 1e-4, "modified Newton drifts: {r:.3e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_knob_parses() {
|
||||
// Only the parser (the knob is read when a stepper is built).
|
||||
assert_eq!(TangentSolver::default(), TangentSolver::BandedLu);
|
||||
assert_eq!(
|
||||
TangentSolver::SPARSE,
|
||||
TangentSolver::SparseLdlt { reuse: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Instruments
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `NXxNYxNZ:span:free|ps` entries (as in `flag3d_structure.rs`); `2d`
|
||||
/// = the 2-D 35×2 Quad8 flag.
|
||||
fn parse_configs(spec: &str) -> Vec<Option<(usize, usize, usize, f64, LateralFaces)>> {
|
||||
spec.split(',')
|
||||
.map(|entry| {
|
||||
if entry.trim() == "2d" {
|
||||
return None;
|
||||
}
|
||||
let mut parts = entry.trim().split(':');
|
||||
let mesh = parts.next().unwrap();
|
||||
let span: f64 = parts.next().unwrap().parse().unwrap();
|
||||
let lateral = match parts.next().unwrap() {
|
||||
"free" => LateralFaces::Free,
|
||||
"ps" => LateralFaces::PlaneStrain,
|
||||
other => panic!("lateral {other}"),
|
||||
};
|
||||
let n: Vec<usize> = mesh.split('x').map(|t| t.parse().unwrap()).collect();
|
||||
Some((n[0], n[1], n[2], span, lateral))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn tag(cfg: &Option<(usize, usize, usize, f64, LateralFaces)>) -> String {
|
||||
match cfg {
|
||||
None => "2d_35x2".to_string(),
|
||||
Some((nx, ny, nz, span, lateral)) => {
|
||||
let l = if *lateral == LateralFaces::Free {
|
||||
"free"
|
||||
} else {
|
||||
"ps"
|
||||
};
|
||||
format!("{nx}x{ny}x{nz}_s{span}_{l}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_flag(cfg: &(usize, usize, usize, f64, LateralFaces)) -> Flag3d {
|
||||
let (nx, ny, nz, span, _) = *cfg;
|
||||
Flag3d::build(Flag3dSpec::turek_hron(span, -0.5 * span, nx, ny, nz)).unwrap()
|
||||
}
|
||||
|
||||
fn analysis_for(
|
||||
cfg: &Option<(usize, usize, usize, f64, LateralFaces)>,
|
||||
flag: Option<&Flag3d>,
|
||||
dt: f64,
|
||||
solver: TangentSolver,
|
||||
) -> NonlinearDynamicAnalysis {
|
||||
match cfg {
|
||||
None => csm3_2d(dt, solver),
|
||||
Some(c) => csm3_3d(flag.unwrap(), c.4, dt, solver),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "instrument: G1 — sparse vs banded, CSM1 static and a CSM3 march"]
|
||||
fn r8g_g1_equivalence() {
|
||||
let out = env_str("R8G_OUT", ".");
|
||||
let configs = parse_configs(&env_str("R8G_CONFIGS", "35x2x1:0.05:ps,35x2x8:0.41:free"));
|
||||
let steps = env_num("R8G_STEPS", 200.0) as usize;
|
||||
let dt = env_num("R8G_DT", 0.005);
|
||||
let do_csm1 = env_str("R8G_CSM1", "1") == "1";
|
||||
let mut table = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(format!("{out}/g1_table.txt"))
|
||||
.unwrap();
|
||||
for cfg in &configs {
|
||||
let name = tag(cfg);
|
||||
let flag = cfg.as_ref().map(build_flag);
|
||||
if do_csm1 {
|
||||
if let (Some(c), Some(flag)) = (cfg, flag.as_ref()) {
|
||||
let forces = gravity_forces(&flag.mesh, RHO_CSM, G);
|
||||
let mut results = Vec::new();
|
||||
for solver in [TangentSolver::BandedLu, TangentSolver::SPARSE] {
|
||||
let start = Instant::now();
|
||||
let analysis = flag
|
||||
.dynamic_analysis(E_MOD, NU, RHO_CSM, c.4, 1e4, 1, 0.5)
|
||||
.with_convergence_criteria(static_criteria())
|
||||
.with_tangent_solver(solver);
|
||||
let mut stepper = analysis.stepper().unwrap();
|
||||
let n = stepper.rest_state().unwrap().displacement.len();
|
||||
let mut u = DVector::zeros(n);
|
||||
let mut newton = 0;
|
||||
for s in 1..=5 {
|
||||
let scale = s as f64 / 5.0;
|
||||
let scaled: Vec<_> =
|
||||
forces.iter().map(|(id, f)| (*id, f * scale)).collect();
|
||||
stepper.set_nodal_forces(&scaled);
|
||||
let state = DynamicState {
|
||||
displacement: u.clone(),
|
||||
velocity: DVector::zeros(n),
|
||||
acceleration: DVector::zeros(n),
|
||||
};
|
||||
let (next, it) = stepper.step(&state).unwrap();
|
||||
newton += it;
|
||||
u = next.displacement;
|
||||
}
|
||||
let d = stepper.node_dofs(flag.point_a());
|
||||
results.push((
|
||||
u.clone(),
|
||||
u[d[0]],
|
||||
u[d[1]],
|
||||
newton,
|
||||
start.elapsed().as_secs_f64(),
|
||||
));
|
||||
}
|
||||
let r = rel_max(&results[1].0, &results[0].0);
|
||||
let line = format!(
|
||||
"G1 CSM1 {name}: A banded ux {:.9e} uy {:.9e} [{} Newton, {:.1} s] | sparse ux \
|
||||
{:.9e} uy {:.9e} [{} Newton, {:.1} s] | max rel |Δu| {r:.3e}",
|
||||
results[0].1,
|
||||
results[0].2,
|
||||
results[0].3,
|
||||
results[0].4,
|
||||
results[1].1,
|
||||
results[1].2,
|
||||
results[1].3,
|
||||
results[1].4
|
||||
);
|
||||
println!("{line}");
|
||||
writeln!(table, "{line}").unwrap();
|
||||
}
|
||||
}
|
||||
let banded = analysis_for(cfg, flag.as_ref(), dt, TangentSolver::BandedLu);
|
||||
let sparse = analysis_for(cfg, flag.as_ref(), dt, TangentSolver::SPARSE);
|
||||
let start = Instant::now();
|
||||
let (worst, rows) = march_pair(&banded, &sparse, steps);
|
||||
let path = format!("{out}/g1_csm3_{name}_dt{dt}.csv");
|
||||
let mut csv = std::fs::File::create(&path).unwrap();
|
||||
writeln!(csv, "step,rel_u,speedup,newton_banded,newton_sparse").unwrap();
|
||||
for (k, (r, speed, ib, is)) in rows.iter().enumerate() {
|
||||
writeln!(csv, "{},{r:.6e},{speed:.3},{ib},{is}", k + 1).unwrap();
|
||||
}
|
||||
let differ = rows.iter().filter(|r| r.2 != r.3).count();
|
||||
let line = format!(
|
||||
"G1 CSM3 {name} dt {dt}: {steps} steps, max rel |Δu| {worst:.3e}, Newton-count \
|
||||
differences {differ}, {:.0} s → {path}",
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
println!("{line}");
|
||||
writeln!(table, "{line}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "instrument: G2 — cost per Newton iteration and per step"]
|
||||
fn r8g_g2_cost() {
|
||||
let out = env_str("R8G_OUT", ".");
|
||||
let configs = parse_configs(&env_str(
|
||||
"R8G_CONFIGS",
|
||||
"2d,35x2x8:0.41:free,35x2x16:0.41:free",
|
||||
));
|
||||
let modes: Vec<String> = env_str("R8G_MODES", "banded,sparse,sparse:3")
|
||||
.split(',')
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
let steps = env_num("R8G_STEPS", 40.0) as usize;
|
||||
let warm = env_num("R8G_WARM", 20.0) as usize;
|
||||
let dt = env_num("R8G_DT", 0.005);
|
||||
let mut table = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(format!("{out}/g2_table.txt"))
|
||||
.unwrap();
|
||||
for cfg in &configs {
|
||||
let name = tag(cfg);
|
||||
let flag = cfg.as_ref().map(build_flag);
|
||||
let mut reference: Option<DVector<f64>> = None;
|
||||
for mode in &modes {
|
||||
let solver = match mode.as_str() {
|
||||
"banded" => TangentSolver::BandedLu,
|
||||
"sparse" => TangentSolver::SPARSE,
|
||||
other => TangentSolver::SparseLdlt {
|
||||
reuse: other.strip_prefix("sparse:").unwrap().parse().unwrap(),
|
||||
},
|
||||
};
|
||||
let analysis = analysis_for(cfg, flag.as_ref(), dt, solver);
|
||||
let t_build = Instant::now();
|
||||
let mut stepper = analysis.stepper().unwrap();
|
||||
let mut state = stepper.rest_state().unwrap();
|
||||
let build = t_build.elapsed().as_secs_f64();
|
||||
// Warm-up steps (off the clock) move the flag off rest.
|
||||
for _ in 0..warm {
|
||||
state = stepper.step(&state).unwrap().0;
|
||||
}
|
||||
let before = stepper.tangent_stats();
|
||||
let mut times = Vec::with_capacity(steps);
|
||||
let mut newton = 0usize;
|
||||
for _ in 0..steps {
|
||||
let t = Instant::now();
|
||||
let (next, it) = stepper.step(&state).unwrap();
|
||||
times.push(t.elapsed().as_secs_f64());
|
||||
newton += it;
|
||||
state = next;
|
||||
}
|
||||
let total: f64 = times.iter().sum();
|
||||
let mut sorted = times.clone();
|
||||
sorted.sort_by(f64::total_cmp);
|
||||
let median = sorted[sorted.len() / 2];
|
||||
let drift = reference
|
||||
.as_ref()
|
||||
.map_or(f64::NAN, |r| rel_max(&state.displacement, r));
|
||||
if reference.is_none() {
|
||||
reference = Some(state.displacement.clone());
|
||||
}
|
||||
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) {}",
|
||||
a.assemblies - b.assemblies,
|
||||
(a.assembly_seconds - b.assembly_seconds)
|
||||
/ (a.assemblies - b.assemblies).max(1) as f64,
|
||||
a.factorizations - b.factorizations,
|
||||
(a.factor_seconds - b.factor_seconds)
|
||||
/ (a.factorizations - b.factorizations).max(1) as f64,
|
||||
a.solves - b.solves,
|
||||
(a.solve_seconds - b.solve_seconds) / (a.solves - b.solves).max(1) as f64,
|
||||
a.fallbacks,
|
||||
a.nnz_l
|
||||
),
|
||||
_ => String::new(),
|
||||
};
|
||||
let line = format!(
|
||||
"G2 {name} {mode} dt {dt}: dofs {} | build+rest {build:.3} s | {steps} steps after \
|
||||
{warm} warm: {:.4} s/step mean, {median:.4} median, {:.2} Newton/step, {:.4} \
|
||||
s/Newton | final rel to first mode {drift:.2e} | threads {}{breakdown}",
|
||||
state.displacement.len(),
|
||||
total / steps as f64,
|
||||
newton as f64 / steps as f64,
|
||||
total / newton.max(1) as f64,
|
||||
rayon::current_num_threads()
|
||||
);
|
||||
println!("{line}");
|
||||
writeln!(table, "{line}").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user