Skip to content

Weak-form vocabulary

Everything you write inside jno.fem([...]) is a symbolic term built from a small set of primitives, composed by ordinary arithmetic. This page is a reference for what you can already write — the declarative building blocks — so you reach for an escape hatch only when you truly need one. A term's region is carried by the symbols you bind into it (no string kwargs): bind a field to an interior/boundary tag and the term assembles there.

d = jno.domain(box(0, 0, 1, 1), mesh_size=0.05)
u, v = d.fem_symbols()                       # trial, test
xi, yi = d.variable("interior", split=True)  # coordinates carry the region
ui, vi = u.bind(x=xi, y=yi), v.bind(x=xi, y=yi)
fem = jno.fem([ui.x * vi.x + ui.y * vi.y - f * vi, u(xb, yb) - 0.0])   # ∇u·∇v = f, Dirichlet

Fields, coordinates, derivatives

You want Write
trial/test fields (scalar/vector, P1/P2, RT/Nédélec) u, v = d.fem_symbols(value_shape=(2,), order=2, space="RT", names=("u","v"))
coordinates + region xi, yi, ti = d.variable("interior", split=True); bind via u.bind(x=xi, y=yi, t=ti) or u(xi, yi)
partial derivatives (any order) ui.x, ui.y, ui.t, ui.xy, ui.xx
grad / div / curl / laplacian / hessian ui.grad(xi, yi), ui.div(xi, yi), ui.curl(xi, yi), ui.laplacian(xi, yi), ui.hessian(xi, yi)

Algebra & nonlinear physics

Closed-form nonlinear physics is already symbolic — + - * / ** and the jno.np library compose straight into a term:

D = k * (1e-3 + ui.x**2 + ui.y**2) ** 0.3          # nonlinear diffusivity D(|∇u|)
react = ui * ui * vi                                 # u² reaction (nonlinear)
fem = jno.fem([ui.t*vi + D*(ui.x*vi.x + ui.y*vi.y) + react - f*vi, ...])
Group Primitives (jno.np.*)
elementary exp log sin cos tan sqrt cbrt abs sign square power (and **)
conditional / piecewise where(cond, a, b), maximum(a, b), minimum(a, b), comparisons u > 0
vector / tensor inner(a, b, n_contract=), dot cross outer trace sym; vector .norm() .dot() .cross()
matrix / Voigt / complex MatrixView (.det .inv .eigvals .sym), VoigtView (.von_mises .deviatoric .invariants), ComplexView (.real .imag .conj)

Integrals (local and non-local)

energy = (ui.x**2 + ui.y**2).integrate()            # ∫_Ω |∇u|² dΩ  (scalar)
heat   = (u * v).integrate(ti)                       # ∫ u·v dt      (time window)

# non-local / Fredholm kernel: ∫ K(x,y) u(y) dy, returned per collocation point
x, _ = d.variable("interior"); y, _ = d.variable("interior")
Ku = (kernel(x, y) * u_of(y)).integrate(var=x)

expr.integrate() is a domain (or boundary) integral; expr.integrate(var=x) is the non-local kernel form (one value per collocation point — Fredholm-type operators); expr.integrate(t) is a time-window integral.

Geometry symbols

Symbol Meaning Use
d.variable(tag, normals=True, split=True)nx, ny boundary outward normal flux / Robin terms nx*ui.x + ny*ui.y
d.cell_size element size h (|detJ|^(1/dim)) at quad points SUPG/GLS stabilization τ = h/(2·|β|)
d.enclosure(tags) view-factor matrix + measures grey-body radiation (.view_factor, .field(), .load())
# SUPG-stabilized advection–diffusion, fully declarative:
h    = d.cell_size
beta = jno.np.vector(1.0, 0.5)
tau  = h / (2 * beta.norm())
adv  = beta[0]*ui.x + beta[1]*ui.y
fem  = jno.fem([adv*vi + nu*(ui.x*vi.x + ui.y*vi.y) - f*vi
                + tau * adv * (beta[0]*vi.x + beta[1]*vi.y), u(xb, yb) - 0.0])

Coefficients & the escape hatch

You want Write
learnable PDE coefficient (inverse design) k = jno.np.parameter((1,), name="k")
fixed coefficient / field / table jno.np.constant(...), d.variable(tag, sample=array)
arbitrary JAX not covered above jno.fn(lambda a, b: ..., [ui, vi]) → a traced node usable in any term

jno.fn is the escape hatch: any differentiable JAX function of traced arguments becomes a term. Reach for it only when the math isn't expressible from the primitives above — which, with conditionals, non-local integrals, the geometry symbols, and the full tensor calculus, is rare.

Introspection

fem.term_kinds (provisional) classifies each PDE term — is_local (pointwise reaction/mass) vs. global (neighbour-coupling diffusion/advection), its temporal order, trial/test gradient channel, and linearity — the basis for operator-splitting routing. See fem.md.