Implicit solver: reference points, couplings, moments, wedge6 (M4) (#26)

* fem: wedge6 element (triangle × interval, 3 × 2 points), patch and cantilever tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* solver: reference points, kinematic and distributing couplings, moments by constraint elimination

Coupled reference points get six degrees of freedom; kinematic couplings are rigid
links, distributing couplings least-squares interpolation constraints. Every
constraint row and prescribed value is eliminated master-slave (Abel-Shephard
pivot choice), so the reduced matrix stays SPD; no coupling gives the old
elimination bit for bit. Verified: rigid motion of a coupled block, clamping
through a point, distributing tension vs pressure, bending and tube torsion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: reference point results in solve.static and solve.probe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* docs: couplings, moments and wedge6 in the solver docs, literature and roadmap

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* solver: simplify constraint elimination and reference point plumbing

Rows combine by one sort; T_f transposed once; K and M reduced in one pass on a
shared pattern; prescribed motion computed once; reference point loads and BCs go
through Resolved like node ones (Mesh::label_dofs); one dof-direction rule;
StepResult::locate for probes; dead Csr::restrict removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* solver: refuse oversized reductions before allocating them

A held distributing coupling point makes a dense (3N)^2 block; estimate it from
the constraint rows and refuse it against the factorization's memory budget
with a clear message, before anything is built. Other reductions with
dependents count their entries first (stopping at the budget) and reserve
fallibly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 18:21:52 -04:00
1 parent a292212b67
commit 48039639cb
18 files changed
+1502 -199

No files matched your search

+24 -2
View File
@@ -9,7 +9,11 @@
//! N = ⅛(1 + ξξᵢ)(1 + ηηᵢ)(1 + ζζᵢ)(ξξᵢ + ηηᵢ + ζζᵢ − 2), a mid-side node with ξᵢ = 0
//! N = ¼(1 − ξ²)(1 + ηηᵢ)(1 + ζζᵢ), and likewise), mid-side nodes 9–12 on the bottom edges,
//! 13–16 on the top edges, 17–20 on the vertical edges (mid-side positions come from
//! [`modelcraft_model::ElementKind::edges`], the one place the node order is written down).
//! [`modelcraft_model::ElementKind::edges`], the one place the node order is written down);
//! - the six-node wedge (pentahedron) as the product of the linear triangle {ξ, η ≥ 0,
//! ξ + η ≤ 1} (area coordinates L₁ = 1 − ξ − η, L₂ = ξ, L₃ = η) and the linear interval
//! ζ ∈ [−1, 1]: nodes 1–3 at ζ = −1 counter-clockwise seen from above, nodes 4–6 above them,
//! Nᵢ = Lᵢ (1 − ζ)/2 and Nᵢ₊₃ = Lᵢ (1 + ζ)/2.
//!
//! The functions are the textbook ones (O. C. Zienkiewicz, R. L. Taylor, J. Z. Zhu, *The Finite
//! Element Method: Its Basis and Fundamentals*, 7th ed., Butterworth-Heinemann 2013, ch. 6;
@@ -47,6 +51,9 @@ impl Shape {
pub const HEX_CORNERS: [[f64; 3]; 8] =
[[-1., -1., -1.], [1., -1., -1.], [1., 1., -1.], [-1., 1., -1.], [-1., -1., 1.], [1., -1., 1.], [1., 1., 1.], [-1., 1., 1.]];
/// Natural coordinates of the wedge's corners.
pub const WEDGE_CORNERS: [[f64; 3]; 6] = [[0., 0., -1.], [1., 0., -1.], [0., 1., -1.], [0., 0., 1.], [1., 0., 1.], [0., 1., 1.]];
/// Natural coordinates of the tetrahedron's corners.
pub const TET_CORNERS: [[f64; 3]; 4] = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.]];
@@ -95,6 +102,20 @@ pub fn hex8(xi: [f64; 3]) -> Shape {
s
}
/// Six-node wedge: the linear triangle times the linear interval.
pub fn wedge6(xi: [f64; 3]) -> Shape {
let mut s = Shape::new(6);
let l = [1.0 - xi[0] - xi[1], xi[0], xi[1]];
let dl = [[-1.0, -1.0], [1.0, 0.0], [0.0, 1.0]];
for (h, f, df) in [(0, (1.0 - xi[2]) / 2.0, -0.5), (3, (1.0 + xi[2]) / 2.0, 0.5)] {
for i in 0..3 {
s.n[h + i] = l[i] * f;
s.dn[h + i] = [dl[i][0] * f, dl[i][1] * f, l[i] * df];
}
}
s
}
/// Twenty-node serendipity hexahedron.
pub fn hex20(xi: [f64; 3]) -> Shape {
let mut s = Shape::new(20);
@@ -147,6 +168,7 @@ mod tests {
v
}
"hex8" => HEX_CORNERS.to_vec(),
"wedge6" => WEDGE_CORNERS.to_vec(),
_ => {
let mut v = HEX_CORNERS.to_vec();
for e in ElementKind::Hex20.edges() {
@@ -159,7 +181,7 @@ mod tests {
#[test]
fn kronecker_delta_partition_of_unity_and_derivatives() {
let kinds: [(&str, fn([f64; 3]) -> Shape); 4] = [("tet4", tet4), ("tet10", tet10), ("hex8", hex8), ("hex20", hex20)];
let kinds: [(&str, fn([f64; 3]) -> Shape); 5] = [("tet4", tet4), ("tet10", tet10), ("wedge6", wedge6), ("hex8", hex8), ("hex20", hex20)];
for (name, f) in kinds {
let nodes = nodes_of(name);
for (i, p) in nodes.iter().enumerate() {