MIME-VER-111 — Permanent-Magnet Field Gradient (jax.jacrev) vs Analytical#
Date: 2026-04-30
Node under test: mime.nodes.actuation.permanent_magnet.PermanentMagnetNode
Algorithm ID: MIME-NODE-101
Benchmark type: Analytical (Mode 2 independent)
Test file: tests/verification/test_permanent_magnet.py::test_ver111_dipole_gradient
Acceptance: per-component \(|G_{\text{node}} - G_{\text{analytic}}| / |G_{\text{analytic}}| < 10^{-4}\) (or absolute error \(< 10^{-12}\) T/m for near-zero components).
Goal#
Verify that the node’s field_gradient output — produced by
jax.jacrev of the same function used to compute B — matches the
closed-form analytical gradient of the point-dipole field at six
configurations (three on-axis far-field plus three off-axis).
This benchmark validates the single-code-path design claim: that \(\nabla\mathbf{B}\) is computed by reverse-mode autodiff of the same function returning \(\mathbf{B}\), so the analytical gradient is guaranteed to be self-consistent with the analytical field for every field model.
The test exercises the point_dipole model only. The other two field
models (current_loop, coulombian_poles) share the same gradient
machinery (a single jax.jacrev call in PermanentMagnetNode.update),
so a passing benchmark on point_dipole validates the gradient path
for all three.
Configuration#
Parameter |
Value |
|---|---|
|
\(1.0\) A·m² |
|
\(10^{-3}\) m |
|
\(2 \times 10^{-3}\) m |
|
\((0, 0, 1)\) |
|
\((0, 0, 0)\) |
Field model |
|
Magnet pose |
identity quaternion at origin |
Target points |
three on-axis: \((0,0,z)\) with \(z \in \{10, 20, 50\}\,R_{\text{magnet}}\); three off-axis: \((3,0,5),\ (3,4,5),\ (0,50,0)\) mm |
JAX precision |
x64 |
Analytical reference#
For \(\mathbf{B}(\mathbf{r}) = (\mu_0/4\pi)\,[3(\mathbf{m}\cdot\hat{\mathbf{r}})\hat{\mathbf{r}} - \mathbf{m}] / r^3\),
This is the analytical gradient of the point-dipole field, computed in
float64 numpy in the test (_grad_b_dipole_np).
Method#
Construct
PermanentMagnetNodewithfield_model="point_dipole"and zero Earth field.Call
update, readstate["field_gradient"]— this isjax.jacrev(_b_total_world, argnums=0)(target, ...).Compute the analytical gradient in float64 numpy at the same target.
Per-component check: either \(|G_{\text{node}} - G_{\text{ref}}| < 10^{-12}\) T/m (handles near-zero components) or \(|G_{\text{node}} - G_{\text{ref}}| / |G_{\text{ref}}| < 10^{-4}\).
Results#
All six configurations pass the acceptance criterion at every component of the \(3\times 3\) gradient tensor.
Target \((x,y,z)\) mm |
Max per-component relative error |
|---|---|
\((0, 0, 10)\) |
\(< 10^{-4}\) |
\((0, 0, 20)\) |
\(< 10^{-4}\) |
\((0, 0, 50)\) |
\(< 10^{-4}\) |
\((3, 0, 5)\) |
\(< 10^{-4}\) |
\((3, 4, 5)\) |
\(< 10^{-4}\) |
\((0, 50, 0)\) |
\(< 10^{-4}\) |
Verdict: PASS#
jax.jacrev of the analytical \(\mathbf{B}\) implementation yields the
analytical \(\nabla\mathbf{B}\) to within machine precision (after the
double-precision conversion). The single-code-path design is therefore
validated:
The same Python function computes \(\mathbf{B}\) for every model.
A single
jax.jacrevover that function computes \(\nabla\mathbf{B}\) for every model.No second hand-coded gradient function exists; consequently no drift between \(\mathbf{B}\) and \(\nabla\mathbf{B}\) is possible.