Files
OneCAD-Simulation/crates/fem/src/jet.rs
T
bd55abea7f FEM foundation A: DofKind/DofLayout, shape-function library with Hessians, solvers on DofLayout (#35)
* 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>
2026-10-13 19:54:32 -04:00

184 lines
4.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);
}
}