Skip to main content

jNO logo

Dev Docs Tests Coverage License Citation Docker image available arXiv Paper

Features · Install · Example · Docs · Citation

jNO (jax Numerical Operators) is a JAX-native library for differentiable numerical methods. Classical solvers — finite elements, finite differences, and spectral (RCWA) — and scientific machine learning — PINNs, neural operators, Bayesian inference — are two pillars on one substrate: you write the math (a weak form, a strong-form stencil, a PDE residual, a data loss), and it lowers to a single GPU-ready, end-to-end reverse-mode-differentiable, jit-compiled graph.

Because every solve is differentiable, the things that are usually separate frameworks are the same tool here: an inverse problem, a PDE-constrained optimization, and a neural-network coefficient are one composition away from a forward solve — no glue code, no finite differences, no leaving JAX.

[!NOTE] Research-level repository under active development. The public API is stabilising but may change between minor versions. Parts of the numerical-methods stack are marked experimental below — the scope and known limitations are stated on each docs page.

What you can do with jNO

Pillar 1 — Differentiable numerical methods

Capability Maturity Notes
FEM, nodal — jno.fem stable Lagrange P1 / P2 / P3+, 2-D & 3-D; steady (linear + Newton), transient (θ-method), second-order-in-time (wave / elastodynamics), complex, periodic, coupled multifield
FEM, non-nodal — H(div) / H(curl) / C¹ experimental Raviart–Thomas (H(div)) and Nédélec edge (H(curl) — Maxwell, eddy currents) elements; C¹ Hermite / Argyris / Morley (plates, biharmonic)
FDM — jno.fdm stable Strong-form collocation from a term list; structured grids + geometric multigrid; unstructured meshes; periodic, coupled, flux BCs; 2-D & 3-D
Spectral / RCWA — jno.rcwa stable Vector-Maxwell RCWA, anisotropic media, Jones / polarization readout
Linear & nonlinear solvers — jno.solve / jno.precond stable Sparse-direct LU, Jacobi-BiCGStab, GMRES, CG, MINRES, Chebyshev, geometric multigrid, optional GPU AMG — matrix-free and differentiable
Generalized eigenproblems — fem.eigs beta K x = λ M x, differentiable, M-orthonormal
Time integration stable θ-method (backward-Euler / Crank–Nicolson), exponential integrators, adaptive step size
Adaptive meshing — jno.solve.remesh / relocate, and coord.d(t) - v as a term beta Hessian-metric remeshing (AFEM), r-adaptivity, moving meshes stated in the term list
Differentiable inverse / PDE-constrained stable Recover a scalar, a field k(x), the geometry, or a neural coefficient through any solve — the gradient flows through the whole march
Geometry — jno.Shape / jno.Path stable CSG via gmsh-OCC; conforming multi-material regions

Pillar 2 — Scientific machine learning

Capability Maturity Notes
Forward PINNs (residual minimisation) stable Hard or soft BC enforcement
Variational PINNs (weak-form losses) stable Network trial functions against the FEM weak form
Operator learning (DeepONet, FNO, U-Net, PROSE via foundax) stable PDE-residual or data-driven
Adaptive resampling (RAD, RARD, CR3, R3, pinnfluence) stable
Stochastic PDEs & noise nodes (gaussian / uniform / laplace) stable Fokker–Planck, stochastic forcing
Bayesian PINNs (NUTS, HMC, MALA, SGLD, SGHMC, VI) stable model.bayesian(kernel_factory) mirrors .optimizer() — per-parameter, mixed freely
Parameter-efficient fine-tuning (LoRA, DoRA, rsLoRA, PiSSA, VeRA, LoKr, OFT, IA3) stable Chain .lora(...) on any wrapped model
Training explainability (gradient conflict, NTK, Hessian, loss landscape, input sensitivity) stable
Foundation-model integration (foundax MLPs, transformers, DeepONet, FNO, PROSE) stable Wrap any Equinox module via jno.nn(...)
Hybrid data + model parallelism stable jno.core(..., mesh=(batch, model))
W&B logging + Orbax checkpointing stable
IREE / MLIR compiled inference for deployment stable

One tracing language bridges the two. A weak form, a strong-form stencil, a PDE residual for a network, and a supervised loss all lower to the same differentiable, jit-compiled graph — so a classical solve, a PINN, and an inverse problem compose rather than living in separate stacks. 29 worked tutorials span elliptic, parabolic, hyperbolic, coupled, inverse, integral, stochastic, FEM / variational, Bayesian, and operator-learning problems — browse the tutorials index.

Install

pip install jax-numerical-operators

One install — FEM, FDM, the solver stack, PINNs, and the scientific-ML tooling all come in the box, running on CPU out of the box. An NVIDIA GPU is one extra away — pip install "jax-numerical-operators[cuda]" — and the heavy, self-contained backends stay behind extras too: [fem] (adaptive remeshing + the PARDISO/cuDSS sparse-direct backends), [rcwa] (the Fourier-modal EM solver), [amg] (GPU algebraic multigrid), [iree], combinable as [cuda,fem]. The Installation guide has the full table, plus Pixi, Docker, and pinning a specific CUDA build.

PS: I recommend pulling the latest main branch to always be up to date!

Example

Differentiable FEM & FDM — one term list, weak or strong form
import jno

d = jno.Shape.rect(0, 0, 1, 1, size=0.05).domain()
xi, yi, _ = d.variable("interior", split=True)
xb, yb, _ = d.variable("boundary", split=True)

# The same BVP two ways — −Δu = 1 on the unit square, u = 0 on the boundary.

# FEM — the WEAK form is the term list (with a test function v):
u, v = d.fem_symbols()
ui, vi = u.bind(x=xi, y=yi), v.bind(x=xi, y=yi)
u_fem = jno.fem([ui.x * vi.x + ui.y * vi.y - 1.0 * vi,   # ∫∇u·∇v − ∫f·v = 0
                 u(xb, yb) - 0.0]).solve()

# FDM — the STRONG form: the same term list, no test function, collocated at the nodes:
w  = d.unknown()
wi = w.bind(x=xi, y=yi)
u_fdm = jno.fdm([-wi.d2(xi) - wi.d2(yi) - 1.0,           # −Δu = 1
                 w(xb, yb) - 0.0]).solve()

# Both are sparse, matrix-free, GPU-ready, and end-to-end differentiable —
# wrap either in an objective to recover a coefficient, a source, or the geometry.
RCWA — a periodic metasurface, from the same constraint list (click to expand)
import jno
import jax.numpy as jnp

# A periodic metasurface unit cell — a patterned high-index slab between two ambients.
K0 = 2 * jnp.pi                                            # vacuum wavenumber (wavelength λ = 1)
d = jno.Shape.box(0, 0, 0, 0.6, 0.6, 1.0, size=0.12).domain()
d.tag("bottom", lambda x, y, z: z < 0.01);  d.tag("top",   lambda x, y, z: z > 0.99)   # z ambients
d.tag("left",   lambda x, y, z: x < 0.01);  d.tag("right", lambda x, y, z: x > 0.59)   # x-periodic
d.tag("front",  lambda x, y, z: y < 0.01);  d.tag("back",  lambda x, y, z: y > 0.59)   # y-periodic

u, v = d.fem_symbols()
xi, yi, zi, _ = d.variable("interior", split=True)
ui, vi = u.bind(x=xi, y=yi, z=zi), v.bind(x=xi, y=yi, z=zi)
def on(tag):                                              # bind u, v on a named face
    s = d.variable(tag, split=True)
    return u.bind(x=s[0], y=s[1], z=s[2]), v.bind(x=s[0], y=s[1], z=s[2])
(ut, vt), (ub, vb) = on("top"), on("bottom")
ul, ur, uf, ubk = on("left")[0], on("right")[0], on("front")[0], on("back")[0]

slab = jno.fn(lambda x, y, z: jnp.where((0.4 < z) & (z < 0.6), 1.0, 0.0), [xi, yi, zi])
eps  = 1.0 + 10.0 * slab      # a patterned slab (swap in jno.np.parameter(...) for inverse design)

# the SAME scalar-Helmholtz list you'd hand jno.fem — rcwa infers period, layers, ε, and incidence:
sol = jno.rcwa([
    ui.x * vi.x + ui.y * vi.y + ui.z * vi.z - K0**2 * eps * (u * vi),   # ∇u·∇v − k₀²·ε·u·v
    -(1j * K0 * ut) * vt,                   # outgoing radiation (top ambient)
    -(1j * K0 * ub - 2j * K0) * vb,         # incident plane wave + radiation (bottom)
    ul - ur,  uf - ubk,                     # Floquet periodicity (x and y)
], orders=200).solve()

sol.efficiency("T")     # transmitted power fraction (needs the [rcwa] backend)
sol.order(+1, 0)        # a chosen diffraction order
Operator learning — a DeepONet trained on a PDE residual (click to expand)
import jno
import jax
import optax
import foundax

dir = jno.setup("./runs/test")

# Domain: `500 *` batches 500 random-coefficient samples of the same geometry
dom = 500 * jno.Shape.rect(0, 0, 2, 1, size=0.05).domain()
x, y, _ = dom.variable("interior")
xb, yb, _ = dom.variable("boundary")
k = dom.variable("k", jax.random.uniform(jax.random.PRNGKey(0), shape=(500, 1, 1), minval=0.5, maxval=1.5))

# Network + optimizer
fx = foundax.deeponet(n_sensors=1, coord_dim=2, basis_functions=32, hidden_dim=128, activation=jax.numpy.tanh)
net = jno.nn(fx)
net.optimizer(optax.adam(optax.schedules.cosine_decay_schedule(1e-3, 20_000, alpha=1e-5)))

# Hard BC enforcement via an output transform; the PDE residual is the loss
u = net(k, jno.np.concat([x, y], axis=-1)) * x * (2 - x) * y * (1 - y)
pde = k * (u.dd(x) + u.dd(y)) + 1.0

crux = jno.core(constraints=[pde.mse], domain=dom)
crux.solve(epochs=20_000, batchsize=32).plot(f"{dir}/training.png")
jno.save(crux, f"{dir}/model.pkl")
PINO — an FNO trained on the PDE residual, with no labelled solutions (click to expand)
import jno
import jax
import optax
import foundax

# `diff_type` declares the differentiation for the whole run. Spectral differentiates the operator's
# OUTPUT along its own grid axes, so the residual needs no autodiff path from x — which is what makes
# a physics loss writable on a field-to-field operator.
dir = jno.setup("./runs/pino", diff_type="spectral")

# Solve −Δu = f on the periodic unit square, for a whole DISTRIBUTION of forcings f at once.
N = 64
dom = 256 * jno.Shape.rect(0, 0, 1, 1, size=1 / N).domain(structured=True)
x, y, _ = dom.variable("interior", split=True)

# The input function: a fresh Gaussian random field per batch — an operator dataset without a dataset.
# (Swap in your own: `f = dom.variable("_f", forcing)` with forcing shaped (samples, H, W, 1),
#  eagerly or as a lazy h5py/zarr handle that streams per batch.)
f = jno.noise.grf(x, y, length_scale=0.2)

grid = (N + 1, N + 1)              # `size=1/N` spans the interval inclusive of both endpoints
net = jno.nn(foundax.fno2d(in_features=1, hidden_channels=32, n_modes=16, d_model=grid))
net.optimizer(optax.adam(1e-3))

u = net(f).bind(x=x, y=y)          # the operator's output, bound to the grid coordinates
pde = u.xx + u.yy + f              # Δu + f = 0 — the two partials fuse into ONE transform pair

crux = jno.core(constraints=[pde.mse], domain=dom)
crux.solve(epochs=20_000, batchsize=8).plot(f"{dir}/training.png")

Citation

If you use jNO in academic work, please cite:

@article{armbruster2026jno,
  title   = {jNO: A JAX Library for Neural Operator and Foundation Model Training},
  author  = {Armbruster, Leon and Ramesh, Rathan and Kruse, Georg and Straub, Christopher},
  journal = {arXiv preprint arXiv:2605.10159},
  year    = {2026},
  doi     = {10.48550/arXiv.2605.10159},
  url     = {https://arxiv.org/abs/2605.10159}
}

AI Disclosure

Parts of this codebase — including model ports, tests, and documentation — were developed with the assistance of AI coding tools. All contributions are reviewed and tested to the best of our ability, but mistakes may remain; please open an issue if you spot one.

Metadata

Release files for jax-numerical-operators 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jax-numerical-operators 0.3.1
File Size Uploaded
jax_numerical_operators-0.3.1.tar.gz 2.2 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for jax-numerical-operators 0.3.1
File Interpreter ABI Platform
jax_numerical_operators-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 3.5 MB

Release files / jax_numerical_operators-0.3.1.tar.gz

Download URL jax_numerical_operators-0.3.1.tar.gz
Size 2.2 MB
Tags Source
SHA-256 checksum
How to use checksums
cdec99da7070b979bc579fd715a2657d670b206ad8df4f96c40affd535f5c78a
BLAKE2b-256 checksum
How to use checksums
aa19be055d23c04709fb9a3f208625c2d6ecf3c8e63359897dce95aaa4196a98
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.

Transparency log

Release files / jax_numerical_operators-0.3.1-py3-none-any.whl

Download URL jax_numerical_operators-0.3.1-py3-none-any.whl
Size 1.3 MB
Tags Python 3
SHA-256 checksum
How to use checksums
168643741797404fb91583a3abb1ffd101a488149c76e0ab0463016e6b75afa4
BLAKE2b-256 checksum
How to use checksums
a810c5f5bc2414d8ee097ea9df3e95f730b3016e8ecd6e6a1fbd99611dabbb10
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.0

2 release files

This release

0.3.1 This release

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page