Poisson 2D (FEM)
A pure finite-element solve of \(-\Delta u = f\) on the unit square with \(u = 0\) on the boundary,
assembled through jno.fem. It is the primer for the API: write the weak form as a list of
residual terms — the volume physics plus the essential boundary condition as u(region) - g —
and solve the assembled A u = b.
The weak form
The trial/test functions come from d.fem_symbols(); binding them to the quadrature
coordinates gives the .x / .y derivatives, and the Dirichlet condition is simply the term
u(xb, yb) - 0.0:
ui, vi = u.bind(x=xi, y=yi), phi.bind(x=xi, y=yi)
f = 2.0 * (xi * (1 - xi) + yi * (1 - yi))
fem = jno.fem([ui.x * vi.x + ui.y * vi.y - f * vi, u(xb, yb) - 0.0], quad_degree=3)
u_fem = jnp.linalg.solve(dense(fem.A), jnp.asarray(fem.b).reshape(-1))
What to notice
- One call,
jno.fem([...]), assembles the stiffnessfem.Aand loadfem.b. - Boundary conditions are terms in the same list — no separate BC objects.
- The solution is audited against the manufactured field \(u^\*=x(1-x)y(1-y)\) (rel-\(L^2 \approx 8\times10^{-3}\)).
Full script
"""01 - 2D Poisson equation assembled + solved through ``jno.fem``.
-Delta u = f on the unit square, u = 0 on the boundary.
Manufactured u*(x, y) = x(1-x) y(1-y), f = -Delta u* = 2[x(1-x) + y(1-y)].
A pure finite-element solve: write the weak form as a list of residual terms (volume physics
+ the essential condition ``u(region) - g``), hand it to ``jno.fem`` -- the single FEM entry --
and solve the assembled ``A u = b``.
"""
import jax.numpy as jnp
import numpy as np
from shapely.geometry import box
import jno
exact = lambda x, y: x * (1 - x) * y * (1 - y) # 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.18)
u, phi = d.fem_symbols()
xi, yi, _ = d.variable("interior", split=True)
xb, yb, _ = d.variable("boundary", split=True)
ui, vi = u.bind(x=xi, y=yi), phi.bind(x=xi, y=yi)
f = 2.0 * (xi * (1 - xi) + yi * (1 - yi))
fem = jno.fem([ui.x * vi.x + ui.y * vi.y - f * vi, u(xb, yb) - 0.0], quad_degree=3) # weak form + u = 0
u_fem = jnp.linalg.solve(dense(fem.A), jnp.asarray(fem.b).reshape(-1)) # solve A u = b
pts = np.asarray(fem.points) # coordinates the DOFs live on
rel_l2 = float(jnp.linalg.norm(exact(pts[:, 0], pts[:, 1]) - u_fem) / jnp.linalg.norm(exact(pts[:, 0], pts[:, 1])))
print(f"\nPoisson via jno.fem: dofs={fem.dofs} rel_L2={rel_l2:.3e}")
assert fem.is_linear and rel_l2 < 5e-2