forked from 1b-agent/fieldcraft-upstream
* fem: DofKind/DofLayout and the shape-function library with second derivatives DofKind (Ux..Rz, T, P), DofSet, ElementDofs (per-node kinds and condensed internal unknowns) and DofLayout: mesh nodes by label, kinds in order, then reference points. basis::Basis covers line2/3, tri3/6, quad4/8/9, tet4/10, wedge6/15, pyramid5, hex8/20/27 with values, first and second natural derivatives (second-order forward AD, fem::jet) and the physical gradients, Hessians and Laplacians with the map's second-derivative term. Tested for Kronecker delta, partition of unity, polynomial completeness, finite differences, affine and curved maps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * solver, explicit: number degrees of freedom with DofLayout The implicit mesh numbers its unknowns with a DofLayout (mesh nodes by label with the kinds their elements declare, then six per coupled reference point); assembly adds element blocks by (node, DofKind), so a node may carry any mix of kinds; couplings, boundary conditions, loads and results address (node, kind). The explicit system keeps node-major translations, now checked against its layout (NODE_DOFS). Solid formulations declare their ElementDofs. Every static, frequency and explicit result of the solver, explicit, engine and io test suites hashes bit-identically before and after (957 results). docs/fem.md describes the DOF model and the shape-function conventions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * fem, solver: simplify after review Block offsets resolved once per node pair; add_scalar/add_diagonal take the kinds like add_block; one checked Jacobian inverse (dense::positive_inverse) for the solid kernels and the basis library; static natural-node tables; the Hessian path inverts the Jacobian once. 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>
184 lines
4.8 KiB
Rust
184 lines
4.8 KiB
Rust
//! Second-order forward-mode automatic differentiation in three variables.
|
||
//!
|
||
//! A [`Jet`] carries a value with its gradient and Hessian with respect to the natural
|
||
//! coordinates (ξ, η, ζ). Arithmetic propagates them by the product and quotient rules, so a
|
||
//! shape function written once as a formula yields its exact first and second derivatives (to
|
||
//! round-off, no finite differences): the hyper-dual / truncated Taylor arithmetic of A. Griewank,
|
||
//! A. Walther, *Evaluating Derivatives*, 2nd ed., SIAM 2008, ch. 13.
|
||
|
||
use std::ops::{Add, Div, Mul, Neg, Sub};
|
||
|
||
/// A value, its gradient and its (symmetric) Hessian.
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub struct Jet {
|
||
pub v: f64,
|
||
pub g: [f64; 3],
|
||
pub h: [[f64; 3]; 3],
|
||
}
|
||
|
||
impl Jet {
|
||
pub const fn constant(v: f64) -> Jet {
|
||
Jet { v, g: [0.0; 3], h: [[0.0; 3]; 3] }
|
||
}
|
||
|
||
/// The independent variable `k` (0–2) at value `v`.
|
||
pub fn var(v: f64, k: usize) -> Jet {
|
||
let mut j = Jet::constant(v);
|
||
if let Some(g) = j.g.get_mut(k) {
|
||
*g = 1.0;
|
||
}
|
||
j
|
||
}
|
||
|
||
/// The three natural coordinates as variables.
|
||
pub fn point(xi: [f64; 3]) -> [Jet; 3] {
|
||
[Jet::var(xi[0], 0), Jet::var(xi[1], 1), Jet::var(xi[2], 2)]
|
||
}
|
||
|
||
/// 1 / self; the Hessian term follows from (1/b)'' = 2 b'b'ᵀ / b³ − b'' / b².
|
||
pub fn recip(self) -> Jet {
|
||
let r = 1.0 / self.v;
|
||
let r2 = r * r;
|
||
let mut out = Jet::constant(r);
|
||
for a in 0..3 {
|
||
out.g[a] = -self.g[a] * r2;
|
||
for b in 0..3 {
|
||
out.h[a][b] = 2.0 * self.g[a] * self.g[b] * r2 * r - self.h[a][b] * r2;
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
fn map(self, other: Jet, f: impl Fn(f64, f64) -> f64) -> Jet {
|
||
let mut out = Jet::constant(f(self.v, other.v));
|
||
for a in 0..3 {
|
||
out.g[a] = f(self.g[a], other.g[a]);
|
||
for b in 0..3 {
|
||
out.h[a][b] = f(self.h[a][b], other.h[a][b]);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
fn scale(self, s: f64) -> Jet {
|
||
Jet { v: self.v * s, g: self.g.map(|x| x * s), h: self.h.map(|r| r.map(|x| x * s)) }
|
||
}
|
||
}
|
||
|
||
impl Add for Jet {
|
||
type Output = Jet;
|
||
fn add(self, o: Jet) -> Jet {
|
||
self.map(o, |a, b| a + b)
|
||
}
|
||
}
|
||
|
||
impl Sub for Jet {
|
||
type Output = Jet;
|
||
fn sub(self, o: Jet) -> Jet {
|
||
self.map(o, |a, b| a - b)
|
||
}
|
||
}
|
||
|
||
impl Neg for Jet {
|
||
type Output = Jet;
|
||
fn neg(self) -> Jet {
|
||
self.scale(-1.0)
|
||
}
|
||
}
|
||
|
||
impl Mul for Jet {
|
||
type Output = Jet;
|
||
fn mul(self, o: Jet) -> Jet {
|
||
let mut out = Jet::constant(self.v * o.v);
|
||
for a in 0..3 {
|
||
out.g[a] = self.v * o.g[a] + o.v * self.g[a];
|
||
for b in 0..3 {
|
||
out.h[a][b] = self.v * o.h[a][b] + o.v * self.h[a][b] + self.g[a] * o.g[b] + o.g[a] * self.g[b];
|
||
}
|
||
}
|
||
out
|
||
}
|
||
}
|
||
|
||
impl Div for Jet {
|
||
type Output = Jet;
|
||
#[allow(clippy::suspicious_arithmetic_impl)]
|
||
fn div(self, o: Jet) -> Jet {
|
||
self * o.recip()
|
||
}
|
||
}
|
||
|
||
impl Add<f64> for Jet {
|
||
type Output = Jet;
|
||
fn add(mut self, c: f64) -> Jet {
|
||
self.v += c;
|
||
self
|
||
}
|
||
}
|
||
|
||
impl Sub<f64> for Jet {
|
||
type Output = Jet;
|
||
fn sub(mut self, c: f64) -> Jet {
|
||
self.v -= c;
|
||
self
|
||
}
|
||
}
|
||
|
||
impl Mul<f64> for Jet {
|
||
type Output = Jet;
|
||
fn mul(self, c: f64) -> Jet {
|
||
self.scale(c)
|
||
}
|
||
}
|
||
|
||
impl Add<Jet> for f64 {
|
||
type Output = Jet;
|
||
fn add(self, j: Jet) -> Jet {
|
||
j + self
|
||
}
|
||
}
|
||
|
||
impl Sub<Jet> for f64 {
|
||
type Output = Jet;
|
||
fn sub(self, j: Jet) -> Jet {
|
||
-j + self
|
||
}
|
||
}
|
||
|
||
impl Mul<Jet> for f64 {
|
||
type Output = Jet;
|
||
fn mul(self, j: Jet) -> Jet {
|
||
j.scale(self)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn products_and_quotients_differentiate_exactly() {
|
||
let [x, y, z] = Jet::point([0.3, -0.7, 1.9]);
|
||
// f = x² y / (1 + z) + 3 x z.
|
||
let f = x * x * y / (1.0 + z) + 3.0 * x * z;
|
||
let (xv, yv, zv) = (0.3, -0.7, 1.9);
|
||
let d = 1.0 + zv;
|
||
assert!((f.v - (xv * xv * yv / d + 3.0 * xv * zv)).abs() < 1e-15);
|
||
let g = [2.0 * xv * yv / d + 3.0 * zv, xv * xv / d, -xv * xv * yv / (d * d) + 3.0 * xv];
|
||
let h = [
|
||
[2.0 * yv / d, 2.0 * xv / d, -2.0 * xv * yv / (d * d) + 3.0],
|
||
[2.0 * xv / d, 0.0, -xv * xv / (d * d)],
|
||
[-2.0 * xv * yv / (d * d) + 3.0, -xv * xv / (d * d), 2.0 * xv * xv * yv / (d * d * d)],
|
||
];
|
||
for a in 0..3 {
|
||
assert!((f.g[a] - g[a]).abs() < 1e-14, "g{a}");
|
||
for b in 0..3 {
|
||
assert!((f.h[a][b] - h[a][b]).abs() < 1e-14, "h{a}{b}: {} vs {}", f.h[a][b], h[a][b]);
|
||
}
|
||
}
|
||
let s = 2.0 - x * 0.5 - (y - 1.0);
|
||
assert_eq!(s.g, [-0.5, -1.0, 0.0]);
|
||
assert_eq!(s.v, 2.0 - 0.15 + 1.7);
|
||
}
|
||
}
|