forked from 1b-agent/fieldcraft-upstream
* Renumber the layer table for solver crates: fem at L1, solver at L2, fsi at L3 L0 model -> L1 geometry, mesh, fem, decks, render -> L2 solver (explicit, lbm later) -> L3 fsi (later) -> L4 doc -> L5 io -> L6 engine -> L7 ui-egui, mcp -> apps. UI_MIN_LAYER moves to 7, the testkit sits above it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * modelcraft-fem: element kernels for tet4, tet10, hex8 (full, incompatible modes), hex20 (full, reduced) Shape functions, Gauss and conical-product quadrature, stiffness, consistent and HRZ-lumped mass, body forces, consistent pressure loads on faces, integration-point stresses and their least-squares extrapolation to nodes, isotropic and orthotropic elasticity. The element table is one row per kind, so wedges, shells and beams are added locally. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * modelcraft-solver: linear static and frequency analysis with an analytic verification suite Assembly from the resolved model on the node graph, boundary conditions by elimination, consistent pressure, gravity and body-force loads, nested-dissection ordering, sparse up-looking Cholesky with a factor-size cap, reactions, integration-point and nodal-averaged stresses, von Mises; shift-invert block Lanczos with full reorthogonalization for natural modes, participation and effective mass. ADR 0001 records the own-linear-algebra decision (determinism, wasm). Verified against closed forms: displacement and traction patch tests for every element, cantilever tip deflection (Timoshenko) with convergence rates, Lamé cylinder, Kirsch hole (Kt -> 3), cantilever and free-free beam frequencies with six rigid-body modes. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Engine: solve.static, solve.frequency and solve.probe; results kept in the session Results are stored per step, restored on a failed command and cleared when the model changes. Caps (degrees of freedom, factor entries, Lanczos vectors) may only be lowered; hostile parameters are tested against a solvable session. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: implicit solver architecture, roadmap M4, parity and references Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Simplify the solver: shared face integration, block assembly, cached extrapolation - model: one face integrator (nodal::face_normal_weights) for tractions and pressures - fem: mid-side tables from ElementKind::edges, orthotropic via inverse3, symmetric accumulation without a symmetrize pass, incompatible-mode recovery without K_uu, model's coordinate check, dead table rows removed - solver: one pattern search per node pair (block, scalar and diagonal adds), extrapolation built once per element type, reactions from the prescribed rows only, matrices dropped after restriction, the free-body shift keyed on the factorization error, step selection in the solver, Error::Element naming the element - engine: the solver owns the default step and the shift check; the model comparison runs once Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: results are stamped with the session revision Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
120 lines
5.0 KiB
Rust
120 lines
5.0 KiB
Rust
//! Fill-reducing ordering: nested dissection by coordinate bisection.
|
||
//!
|
||
//! Nested dissection (A. George, *Nested dissection of a regular finite element mesh*, SIAM J.
|
||
//! Numer. Anal. 10(2):345–363, 1973) orders a separator after the two parts it splits, so the
|
||
//! Cholesky factor of a 3D mesh with n nodes fills O(n^{4/3}) instead of the O(n^{5/3}) of a band
|
||
//! or envelope ordering. The separator here is geometric: the nodes are split at the median of
|
||
//! the longest axis of their bounding box, and the nodes of the smaller side that touch the other
|
||
//! side form the separator (the coordinate nested dissection of H. D. Simon, *Partitioning of
|
||
//! unstructured problems for parallel processing*, Computing Systems in Engineering 2:135–148,
|
||
//! 1991, with vertex separators). Ties are broken by node index, so the ordering is
|
||
//! deterministic.
|
||
|
||
/// Parts this small are ordered as they come.
|
||
const LEAF: usize = 16;
|
||
|
||
/// A permutation of the nodes: `order[k]` is the node eliminated k-th.
|
||
pub fn nested_dissection(xyz: &[[f64; 3]], adjacency: &[Vec<usize>]) -> Vec<usize> {
|
||
let n = xyz.len();
|
||
let mut order = Vec::with_capacity(n);
|
||
// 0 = not in the current part, 1 = left, 2 = right.
|
||
let mut side = vec![0u8; n];
|
||
// An explicit stack instead of recursion: (part, separator to emit after it).
|
||
enum Task {
|
||
Split(Vec<usize>),
|
||
Emit(Vec<usize>),
|
||
}
|
||
let mut stack = vec![Task::Split((0..n).collect())];
|
||
while let Some(task) = stack.pop() {
|
||
let mut part = match task {
|
||
Task::Emit(sep) => {
|
||
order.extend(sep);
|
||
continue;
|
||
}
|
||
Task::Split(p) => p,
|
||
};
|
||
if part.len() <= LEAF {
|
||
order.extend(part);
|
||
continue;
|
||
}
|
||
let mut lo = [f64::INFINITY; 3];
|
||
let mut hi = [f64::NEG_INFINITY; 3];
|
||
for &v in &part {
|
||
let p = xyz.get(v).copied().unwrap_or_default();
|
||
for k in 0..3 {
|
||
lo[k] = lo[k].min(p[k]);
|
||
hi[k] = hi[k].max(p[k]);
|
||
}
|
||
}
|
||
let axis = (0..3).fold(0, |best, k| if hi[k] - lo[k] > hi[best] - lo[best] { k } else { best });
|
||
let key = |v: usize| xyz.get(v).map_or(0.0, |p| p[axis]);
|
||
part.sort_by(|&a, &b| key(a).total_cmp(&key(b)).then(a.cmp(&b)));
|
||
let half = part.len() / 2;
|
||
for (i, &v) in part.iter().enumerate() {
|
||
if let Some(s) = side.get_mut(v) {
|
||
*s = if i < half { 1 } else { 2 };
|
||
}
|
||
}
|
||
let touches = |v: usize, other: u8| adjacency.get(v).is_some_and(|row| row.iter().any(|w| side.get(*w) == Some(&other)));
|
||
let (left, right) = part.split_at(half);
|
||
let left_boundary: Vec<usize> = left.iter().copied().filter(|&v| touches(v, 2)).collect();
|
||
let right_boundary: Vec<usize> = right.iter().copied().filter(|&v| touches(v, 1)).collect();
|
||
let separator = if left_boundary.len() <= right_boundary.len() { left_boundary } else { right_boundary };
|
||
for &v in &part {
|
||
if let Some(s) = side.get_mut(v) {
|
||
*s = 0;
|
||
}
|
||
}
|
||
// Mark the separator so the parts leave it out.
|
||
for &v in &separator {
|
||
if let Some(s) = side.get_mut(v) {
|
||
*s = 3;
|
||
}
|
||
}
|
||
let a: Vec<usize> = left.iter().copied().filter(|v| side.get(*v) != Some(&3)).collect();
|
||
let b: Vec<usize> = right.iter().copied().filter(|v| side.get(*v) != Some(&3)).collect();
|
||
for &v in &separator {
|
||
if let Some(s) = side.get_mut(v) {
|
||
*s = 0;
|
||
}
|
||
}
|
||
// Order: a, then b, then the separator (the stack runs in reverse).
|
||
stack.push(Task::Emit(separator));
|
||
stack.push(Task::Split(b));
|
||
stack.push(Task::Split(a));
|
||
}
|
||
order
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn a_permutation_with_separators_last() {
|
||
// A 12 × 12 grid graph.
|
||
let m = 12;
|
||
let id = |i: usize, j: usize| i * m + j;
|
||
let xyz: Vec<[f64; 3]> = (0..m * m).map(|v| [(v / m) as f64, (v % m) as f64, 0.0]).collect();
|
||
let mut adj = vec![Vec::new(); m * m];
|
||
for i in 0..m {
|
||
for j in 0..m {
|
||
for (di, dj) in [(0i64, 1i64), (1, 0), (0, -1), (-1, 0), (0, 0)] {
|
||
let (a, b) = (i as i64 + di, j as i64 + dj);
|
||
if (0..m as i64).contains(&a) && (0..m as i64).contains(&b) {
|
||
adj[id(i, j)].push(id(a as usize, b as usize));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let order = nested_dissection(&xyz, &adj);
|
||
let mut sorted = order.clone();
|
||
sorted.sort_unstable();
|
||
assert_eq!(sorted, (0..m * m).collect::<Vec<_>>());
|
||
// The top separator (last 12 entries) is one grid line.
|
||
let last: Vec<usize> = order[m * m - m..].to_vec();
|
||
assert!(last.iter().all(|v| v / m == last[0] / m) || last.iter().all(|v| v % m == last[0] % m), "{last:?}");
|
||
assert_eq!(order, nested_dissection(&xyz, &adj), "deterministic");
|
||
}
|
||
}
|