forked from 1b-agent/fieldcraft-upstream
Implicit solver: fem kernels (tet4/10, hex8, hex20), sparse Cholesky, block Lanczos, solve.* commands (M4) (#23)
* 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]>
This commit is contained in:
33 files changed
+4559
-53
No files matched your search
+49
-19
@@ -14,8 +14,8 @@ pub enum Class {
|
||||
Layer(u8),
|
||||
/// Layer 0, and additionally may depend on no workspace crate at all.
|
||||
Standalone,
|
||||
/// Test tooling: may depend on anything up to L5 (it sits at L6 for rule
|
||||
/// purposes); other crates may use it only as a dev-dependency.
|
||||
/// Test tooling: may depend on anything up to L7 (it sits at [`TESTKIT_LAYER`]
|
||||
/// for rule purposes); other crates may use it only as a dev-dependency.
|
||||
Testkit,
|
||||
/// Binaries and build tooling: exempt from the rules.
|
||||
Exempt,
|
||||
@@ -26,7 +26,7 @@ impl Class {
|
||||
match self {
|
||||
Class::Layer(l) => Some(l),
|
||||
Class::Standalone => Some(0),
|
||||
Class::Testkit => Some(6),
|
||||
Class::Testkit => Some(TESTKIT_LAYER),
|
||||
Class::Exempt => None,
|
||||
}
|
||||
}
|
||||
@@ -35,17 +35,23 @@ impl Class {
|
||||
/// The layering table. Names are package names without the `modelcraft-`
|
||||
/// prefix.
|
||||
pub const TABLE: &[(&str, Class)] = &[
|
||||
// L0: the solver-neutral model.
|
||||
("model", Class::Layer(0)),
|
||||
// L1: geometry and math kernels.
|
||||
("geometry", Class::Layer(1)),
|
||||
("mesh", Class::Layer(1)),
|
||||
("solver", Class::Layer(1)),
|
||||
("fem", Class::Layer(1)),
|
||||
("decks", Class::Layer(1)),
|
||||
("render", Class::Layer(1)),
|
||||
("doc", Class::Layer(2)),
|
||||
("io", Class::Layer(3)),
|
||||
("engine", Class::Layer(4)),
|
||||
("ui-egui", Class::Layer(5)),
|
||||
("mcp", Class::Layer(5)),
|
||||
// L2: solvers (the explicit and lattice Boltzmann solvers join here).
|
||||
("solver", Class::Layer(2)),
|
||||
// L3: coupling between solvers (`fsi`, later).
|
||||
// L4 and up: the document, files, commands, UI.
|
||||
("doc", Class::Layer(4)),
|
||||
("io", Class::Layer(5)),
|
||||
("engine", Class::Layer(6)),
|
||||
("ui-egui", Class::Layer(7)),
|
||||
("mcp", Class::Layer(7)),
|
||||
("testkit", Class::Testkit),
|
||||
// apps and tooling
|
||||
("modelcraft", Class::Exempt),
|
||||
@@ -73,7 +79,13 @@ pub const UI_CRATES: &[&str] = &["egui", "eframe", "egui-wgpu", "wgpu", "winit",
|
||||
pub const SOLIDCRAFT_GATEWAY: &str = "geometry";
|
||||
|
||||
/// First layer allowed to use UI crates.
|
||||
pub const UI_MIN_LAYER: u8 = 5;
|
||||
pub const UI_MIN_LAYER: u8 = 7;
|
||||
|
||||
/// The layer the testkit sits at for rule purposes: above every layered crate.
|
||||
pub const TESTKIT_LAYER: u8 = UI_MIN_LAYER + 1;
|
||||
|
||||
/// The layer reported for apps and tooling when a layered crate depends on one.
|
||||
const APPS_LAYER: u8 = TESTKIT_LAYER + 1;
|
||||
|
||||
pub fn short_name(pkg: &str) -> &str {
|
||||
pkg.strip_prefix("modelcraft-").unwrap_or(pkg)
|
||||
@@ -118,7 +130,7 @@ pub enum Violation {
|
||||
Upward { krate: String, dep: String, from: u8, to: u8, kind: DepKind },
|
||||
StandaloneHasWorkspaceDep { krate: String, dep: String },
|
||||
TestkitAsNormalDep { krate: String },
|
||||
UiBelowL5 { krate: String, dep: String, layer: u8 },
|
||||
UiBelowUiLayer { krate: String, dep: String, layer: u8 },
|
||||
SolidCraftOutsideGeometry { krate: String, dep: String },
|
||||
}
|
||||
|
||||
@@ -137,8 +149,8 @@ impl std::fmt::Display for Violation {
|
||||
Violation::TestkitAsNormalDep { krate } => {
|
||||
write!(f, "{krate}: modelcraft-testkit may only be a dev-dependency")
|
||||
}
|
||||
Violation::UiBelowL5 { krate, dep, layer } => {
|
||||
write!(f, "{krate} (L{layer}) depends on UI crate `{dep}`; UI toolkits are only allowed in L5+")
|
||||
Violation::UiBelowUiLayer { krate, dep, layer } => {
|
||||
write!(f, "{krate} (L{layer}) depends on UI crate `{dep}`; UI toolkits are only allowed in L{UI_MIN_LAYER}+")
|
||||
}
|
||||
Violation::SolidCraftOutsideGeometry { krate, dep } => {
|
||||
write!(f, "{krate} depends on `{dep}`; only modelcraft-{SOLIDCRAFT_GATEWAY} may use SolidCraft")
|
||||
@@ -183,7 +195,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
|
||||
krate: c.name.clone(),
|
||||
dep: d.name.clone(),
|
||||
from: layer,
|
||||
to: if to == u8::MAX { 7 } else { to },
|
||||
to: to.min(APPS_LAYER),
|
||||
kind: d.kind,
|
||||
});
|
||||
}
|
||||
@@ -192,7 +204,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
|
||||
} else if d.name.starts_with("solidcraft-") && short_name(&c.name) != SOLIDCRAFT_GATEWAY {
|
||||
out.push(Violation::SolidCraftOutsideGeometry { krate: c.name.clone(), dep: d.name.clone() });
|
||||
} else if layer < UI_MIN_LAYER && is_ui_crate(&d.name) {
|
||||
out.push(Violation::UiBelowL5 { krate: c.name.clone(), dep: d.name.clone(), layer });
|
||||
out.push(Violation::UiBelowUiLayer { krate: c.name.clone(), dep: d.name.clone(), layer });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,12 +271,14 @@ mod tests {
|
||||
#[test]
|
||||
fn upward_dependency_flagged() {
|
||||
let v = check(&[c("modelcraft-doc", &[("modelcraft-engine", Normal, true)])]);
|
||||
assert!(matches!(v[..], [Violation::Upward { from: 2, to: 4, .. }]));
|
||||
assert!(matches!(v[..], [Violation::Upward { from: 4, to: 6, .. }]));
|
||||
let v = check(&[c("modelcraft-fem", &[("modelcraft-solver", Normal, true)])]);
|
||||
assert!(matches!(v[..], [Violation::Upward { from: 1, to: 2, .. }]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sideways_dependency_flagged() {
|
||||
let v = check(&[c("modelcraft-mesh", &[("modelcraft-solver", Normal, true)])]);
|
||||
let v = check(&[c("modelcraft-mesh", &[("modelcraft-fem", Normal, true)])]);
|
||||
assert!(matches!(v[..], [Violation::Upward { from: 1, to: 1, .. }]));
|
||||
}
|
||||
|
||||
@@ -280,10 +294,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_crates_below_l6_flagged() {
|
||||
fn solvers_sit_above_the_kernels() {
|
||||
let g = [
|
||||
c("modelcraft-fem", &[("modelcraft-model", Normal, true)]),
|
||||
c("modelcraft-solver", &[("modelcraft-fem", Normal, true), ("modelcraft-mesh", Normal, true)]),
|
||||
c("modelcraft-engine", &[("modelcraft-solver", Normal, true)]),
|
||||
];
|
||||
assert!(check(&g).is_empty(), "{:?}", check(&g));
|
||||
let v = check(&[c("modelcraft-solver", &[("modelcraft-engine", Normal, true)])]);
|
||||
assert!(matches!(v[..], [Violation::Upward { from: 2, to: 6, .. }]));
|
||||
let v = check(&[c("modelcraft-engine", &[("modelcraft-cli", Normal, true)])]);
|
||||
assert!(matches!(v[..], [Violation::Upward { from: 6, to: APPS_LAYER, .. }]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_crates_below_the_ui_layer_flagged() {
|
||||
for dep in ["egui", "eframe", "winit", "egui_kittest", "rfd", "bevy_ecs", "bevy"] {
|
||||
let v = check(&[c("modelcraft-engine", &[(dep, Normal, false)])]);
|
||||
assert!(matches!(v[..], [Violation::UiBelowL5 { layer: 4, .. }]), "{dep}");
|
||||
assert!(matches!(v[..], [Violation::UiBelowUiLayer { layer: 6, .. }]), "{dep}");
|
||||
let v = check(&[c("modelcraft-solver", &[(dep, Normal, false)])]);
|
||||
assert!(matches!(v[..], [Violation::UiBelowUiLayer { layer: 2, .. }]), "{dep}");
|
||||
}
|
||||
assert!(check(&[c("modelcraft-engine", &[("egui_extras_not", Normal, false)])]).is_empty());
|
||||
assert!(check(&[c("modelcraft-ui-egui", &[("winit", Normal, false)])]).is_empty());
|
||||
|
||||
Reference in new issue
Block a user