Stationary Allen-Cahn (nonlinear FEM)
A nonlinear FEM solve: the stationary Allen-Cahn equation \(-\varepsilon^2 \Delta u + (u^3 - u) = 0\),
whose stable equilibrium is a sharp tanh phase interface (Allen & Cahn, Acta Metall. 1979).
Nonlinear weak form → residual operator
The cubic term (u**3 - u) * vi makes the form nonlinear in u, so jno.fem returns a
residual operator (fem.residual, fem.jacobian) rather than a linear A, b:
fem = jno.fem([eps**2 * (ui.x * vi.x + ui.y * vi.y) + (u**3 - u) * vi,
u(xl, yl) - exact(0.0), u(xr, yr) - exact(1.0)], quad_degree=3)
assert not fem.is_linear
Newton (here SciPy's root) is driven by fem.residual and fem.jacobian, started from an
over-wide interface and sharpened to the analytic equilibrium:
sol = spo.root(lambda v: np.asarray(fem.residual(jnp.asarray(v))), u0,
jac=lambda v: np.asarray(fem.jacobian(jnp.asarray(v))), method="hybr")
What to notice
- A nonlinear weak form switches
jno.femto the residual route automatically. fem.residual(u)andfem.jacobian(u)plug into any Newton / root-finder.- Converges to the
tanhinterface at rel-\(L^2 \approx 10^{-3}\).
Full script
"""02 - Stationary Allen-Cahn via the nonlinear ``jno.fem`` residual route.
-eps^2 Delta u + (u^3 - u) = 0, u = tanh((x - 0.5) / (sqrt(2) eps)) on the left/right walls.
The cubic reaction makes the weak form nonlinear in ``u``, so ``jno.fem`` returns a residual
operator (``fem.residual(u)`` / ``fem.jacobian(u)``) instead of a linear ``A, b``. Newton starts
from a smooth (over-wide) interface and sharpens it to the analytic Allen-Cahn phase interface
(Allen & Cahn, *Acta Metall.* 1979).
"""
import jax.numpy as jnp
import numpy as np
import scipy.optimize as spo
from shapely.geometry import box
import jno
eps = 0.15
exact = lambda x: np.tanh((x - 0.5) / (np.sqrt(2.0) * eps)) # noqa: E731
dense = lambda A: jnp.asarray(A.todense()) if hasattr(A, "todense") else jnp.asarray(A) # noqa: E731
d = jno.domain(box(0.0, 0.0, 1.0, 1.0), mesh_size=0.06)
u, phi = d.fem_symbols()
xi, yi, _ = d.variable("interior", split=True)
xl, yl, _ = d.variable("left", split=True)
xr, yr, _ = d.variable("right", split=True)
ui, vi = u.bind(x=xi, y=yi), phi.bind(x=xi, y=yi)
# eps^2 grad(u).grad(phi) + (u^3 - u) phi = 0 (cubic -> nonlinear -> residual operator)
fem = jno.fem(
[eps**2 * (ui.x * vi.x + ui.y * vi.y) + (u**3 - u) * vi, u(xl, yl) - exact(0.0), u(xr, yr) - exact(1.0)], quad_degree=3
)
assert not fem.is_linear
pts = np.asarray(fem.points)
u0 = np.tanh((pts[:, 0] - 0.5) / (np.sqrt(2.0) * 0.30)) # over-wide interface = the Newton start
sol = spo.root(
lambda v: np.asarray(fem.residual(jnp.asarray(v))),
u0,
jac=lambda v: np.asarray(dense(fem.jacobian(jnp.asarray(v)))),
method="hybr",
)
rel_l2 = float(jnp.linalg.norm(exact(pts[:, 0]) - sol.x) / jnp.linalg.norm(exact(pts[:, 0])))
print(f"\nAllen-Cahn (nonlinear residual route): dofs={fem.dofs} Newton converged={sol.success} rel_L2={rel_l2:.3e}")
assert sol.success and rel_l2 < 1e-2