pinn-rk
Runge–Kutta Physics‑Informed Neural Networks (PINNs) with time‑discrete losses in PyTorch. Ships Gauss–Legendre, Radau IIA and Lobatto IIIA at 2, 3 and 4 stages — classical orders up to 8 — with a boundary‑conditioned neural ansatz, d‑dimensional operators, and end‑to‑end examples for the 1D and 2D heat equations.
See ROADMAP.md for milestones and planned features.
Key features
- Time‑discrete residual built from Runge–Kutta collocation: residuals evaluated at stage nodes and integrated with RK weights.
- General RK backend via
ButcherTableau— nine tableaux included, andcollocation_tableau(nodes)derives a new collocation family from its nodes alone. - Boundary conditioning through a multiplicative factor $\Phi(x)$ to satisfy homogeneous Dirichlet BCs exactly.
- Modular PDE operators with autograd‑based derivatives:
Laplacian1D, andLaplacianNDfor any number of spatial dimensions. - 1D, 2D and 3D domains on the unit cube, with exact homogeneous Dirichlet conditions on every face.
- Practical implementation: type hints, ruff/mypy clean, tests, and GitHub Actions CI.
Installation (development)
# Using Poetry (recommended)
poetry install
pre-commit install
Requires Python 3.10–3.12. Install a CPU or CUDA build of PyTorch appropriate for your environment.
Quick start
Train on the 1D heat equation $u_t - u_{xx} = 0$ on $(0,1)$ with homogeneous Dirichlet BCs and initial condition $u_0(x) = \sin(\pi x)$:
from pinn_rk.examples.train_heat_equation import train_heat_equation, l2_error
import torch
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = train_heat_equation(
method="radau3", # gauss2|radau2|lobatto2 · gauss3|radau3|lobatto3 · gauss4|radau4|lobatto4
T=0.1,
N=20,
n_x_train=256,
steps=1000,
lr=2e-3,
device=device,
)
print("L2(T=0.1) =", l2_error(model, T=0.1, nx=1001, device=device))
Expected output (ballpark): L2(T=0.1) ~ 1e-2 … 1e-1 depending on training steps and hardware.
Concept overview
We consider linear parabolic PDEs of the form
$$ u_t + \mathcal{L}u = f \quad \text{in } \Omega \times (0,T], \qquad u = 0 \text{ on } \partial\Omega, \qquad u(\cdot, 0) = u_0 . $$
The time interval is partitioned into slabs $J_n = [t_n, t_{n+1}]$ with step $k_n$. For a $q$-stage RK method with nodes $c_i$, we form stage times $t_{n,i} = t_n + c_i k_n$ and evaluate the network $u_\theta(x,t)$ and the operator $\mathcal{L}u_\theta$ at these times. The discrete RK residual is accumulated using the quadrature weights $b_i$:
$$ \int_{J_n} \lVert r(t) \rVert^2 , dt ;\approx; k_n \sum_{i=1}^{q} b_i , \lVert r(t_{n,i}) \rVert^2 , \qquad r := \partial_t \hat{u} + \Pi_{q-1}(\mathcal{L}\hat{u}) - \tilde{\Pi}_{q-1} f . $$
Here $\hat{u}$ is a polynomial time interpolant on each slab, and $\Pi_{q-1}$, $\tilde{\Pi}_{q-1}$ are degree $q-1$ projections realized at the collocation nodes.
The residual
Writing the semi-discrete problem as $u' = f - \mathcal{L}u =: F$, the Runge–Kutta method is defined by its stage and update equations:
$$ U_i = u_n + k\sum_j a_{ij} F_j, \qquad u_{n+1} = u_n + k\sum_i b_i F_i . $$
pinn-rk imposes both directly on the network (residual="rk", the default), dividing by $k$ so they carry the units of a time derivative:
$$ r_i = \frac{U_i - u_n}{k} - \sum_j a_{ij} F_j , \qquad r_{\text{step}} = \frac{u_{n+1} - u_n}{k} - \sum_i b_i F_i . $$
This uses the full Butcher tableau — including the coupling matrix $A$ — so the scheme inherits the tableau's own accuracy. Feeding the manufactured solution $u = \sin(\pi x)e^{-\pi^2 t}$ through the residual leaves only local truncation error:
| tableau | stages | stage order | update order | classical $p$ |
|---|---|---|---|---|
lobatto2 |
2 | $1.91$ | $1.91$ | 2 |
radau2 |
2 | $1.92$ | $2.91$ | 3 |
gauss2 |
2 | $1.94$ | $3.91$ | 4 |
lobatto3 |
3 | $2.92$ | $3.91$ | 4 |
radau3 |
3 | $2.92$ | $4.91$ | 5 |
gauss3 |
3 | $2.91$ | $5.91$ | 6 |
lobatto4 |
4 | $3.88$ | $5.85$ | 6 |
radau4 |
4 | $3.84$ | $6.84$ | 7 |
gauss4 |
4 | $3.86$ | — | 8 |
Gauss q=4 is order 8, which is past what double precision can measure here. The update residual is $(u_{n+1}-u_n)/k$, so rounding in $u$ contributes about $\epsilon/k$ — roughly $2.6\times10^{-14}$ at $k=1/120$ — and the true residual falls beneath it. On coarse slabs, where it is still visible, the observed rate is $\approx 7.7$; refining further makes the measured value stall rather than fall, which tests/test_rk_order.py asserts explicitly, so the flat region is not mistaken for a convergence failure.
The update residual recovers each method's classical order, which is what makes the choice of tableau meaningful: Gauss and Radau cost the same stages, and Gauss is orders of magnitude more consistent at the same slab size.
The stage residual converges at the stage order, which for collocation methods equals the number of stages $q$. Since the objective sums both, the stage order sets the ceiling — which is why the stage count matters more than the classical order alone suggests: each added stage lifts that ceiling by one power of $k$, from $\mathcal{O}(k^2)$ at $q=2$ to $\mathcal{O}(k^4)$ at $q=4$. Orders are pinned by tests/test_rk_order.py, and the tableaux themselves are re-derived from their nodes and checked against the order conditions in tests/test_tableau_order.py.
Lobatto IIIA is stiffly accurate — its $b$ equals the last row of $A$ — so its update and final stage residuals coincide exactly.
The interpolant residual (residual="interpolant")
The pre-0.3 formulation is retained for comparison. It reconstructs $\hat{u}$ as a polynomial in time through the stage values, differentiates it analytically via the barycentric differentiation matrix $D_{ij} = L_j'(t_i)$, and imposes $u_t + \mathcal{L}u - f$. It uses only $c$ and $b$, ignoring $A$, so every tableau behaves identically and accuracy follows the degree of $\hat{u}$ rather than the order of the method:
stencil (q_aux) |
$\deg\hat{u}$ | max residual at $k=5\times10^{-3}$ | observed order |
|---|---|---|---|
"same" — stage nodes only |
$q-1$ | $1.6\times10^{-1}$ | $\mathcal{O}(k)$ |
"extend" — plus slab start |
$q$ | $2.6\times10^{-3}$ | $\mathcal{O}(k^2)$ |
For Gauss the RK form is roughly $10^5$ times more consistent than this at the same slab size. q_aux applies only to this setting and is ignored under residual="rk".
What consistency does and does not tell you. The tables above measure truncation error — the residual left on the exact solution — which bounds the best a perfectly trained network could do. It does not predict optimisation behaviour. In short single-seed training runs on the heat-equation example the two forms trade places depending on the tableau and the step budget, and the loss trace is non-monotonic because the spatial sampler redraws each step. Treat the training comparison as unresolved: a fair answer needs several seeds and converged runs.
residualexists so the comparison can be made rather than assumed.
Balancing the two loss terms
The objective sums the PDE residual and the initial-condition penalty. Their relative size is not stable: on the shipped example the penalty is essentially the entire loss at initialisation ($5.1$ against a residual of $1.7\times10^{-4}$) and roughly a fifth of it after a few hundred steps. Training therefore starts by fitting the initial condition almost exclusively. RkPinnConfig.ic_weight scales the penalty so this can be controlled; ic_weight=0.0 drops it entirely and measures the residual alone.
Package layout
src/pinn_rk/
├─ tableau.py # ButcherTableau + Gauss/Radau/Lobatto factories
├─ mesh.py # TimeMesh: partition of [0,T] into slabs
├─ interpolants.py # barycentric weights, Lagrange evaluation
├─ model.py # MLP with boundary-conditioned ansatz
├─ operators.py # elliptic operators (e.g., Laplacian1D)
├─ config.py # RkPinnConfig
├─ loss.py # RkPinnLoss: the time-discrete RK objective
├─ examples/ # reference training routines (heat equation)
├─ __init__.py # public API
└─ __about__.py # version
API reference (essentials)
ButcherTableau
Purpose. Encodes a Runge–Kutta method.
-
Fields:
A: Tensor [q,q],b: Tensor [q],c: Tensor [q]. -
Factories:
butcher_gauss_legendre_q2()– order 4, 2 stagesbutcher_radau_iia_q2()– order 3, 2 stagesbutcher_lobatto_iiia_q2()– trapezoidal rule, 2 stages
TimeMesh
Purpose. Uniform or user‑defined partition of $[0,T]$.
TimeMesh.uniform(T: float, N: int, device) -> TimeMesh- Fields:
nodes: Tensor [N+1],steps: Tensor [N].
MLP
Purpose. Network $g_\theta(x,t)$ used inside the boundary‑conditioned ansatz $u_\theta(x,t) = \Phi(x) , g_\theta(x,t)$ with $\Phi(x) = \prod_i 4x_i(1-x_i)$ on $[0,1]^d$.
The factor of 4 per dimension normalises the peak to 1. Without it $\Phi$ peaks at $4^{-d}$, so the network must grow like $4^d$ to represent an $\mathcal{O}(1)$ solution and the gradients reaching it are attenuated by the same factor. On the 2D heat equation the unnormalised form plateaus near 55% relative error where this one reaches 0.4%.
MLP(in_dim=2, width=128, depth=4, activation="tanh")
RkPinnConfig
Purpose. Configuration for assembling the time‑discrete loss.
- Key fields:
tableau,time_mesh,n_x_train,spatial_sampler,init_data. residual:"rk"(default) imposes the stage and update equations of the full Butcher tableau;"interpolant"selects the pre‑0.3 reconstruction‑derivative form.q_aux:"same"or"extend"— reconstruction stencil, used only whenresidual="interpolant".
RkPinnLoss
Purpose. Computes the RK‑PINN time‑discrete objective over all slabs.
RkPinnLoss(model, Lop, f_rhs, cfg)- Call to compute scalar loss:
loss = loss_fn()
Interpolants
Purpose. Barycentric Lagrange machinery backing the time reconstruction $\hat{u}$.
barycentric_weights(nodes) -> Tensor [q]lagrange_eval(t, nodes, w) -> Tensor [..., q]– basis values $L_j(t)$, for evaluating $\hat{u}$ away from the nodes.differentiation_matrix(nodes, w=None) -> Tensor [q,q]– $D_{ij} = L_j'(t_i)$, exact for polynomials of degree $\le q-1$.
$\partial_t\hat{u}$ at the stage nodes is D @ U, taken analytically from the interpolant. The reconstruction stencil is selected by RkPinnConfig.q_aux: "same" interpolates the $q$ stage values ($\hat{u}$ of degree $q-1$), "extend" also uses the slab start $t_n$ (degree $q$, one extra network evaluation per slab).
Utilities
train_heat_equation(...) -> nn.Module– reference training routine.l2_error(model, T, nx=1001, device) -> float– $L^2$ error at final time.
Choosing the RK scheme
Tableaux with the same stage count cost the same per slab, so within a row of this table accuracy is close to free. Under residual="rk" the trade‑off is the classical one between order and stability:
| scheme | stages | classical order | stability | notes |
|---|---|---|---|---|
lobatto2 (Lobatto IIIA) |
2 | 2 | A‑stable | trapezoidal rule; symmetric, stiffly accurate |
radau2 (Radau IIA) |
2 | 3 | L‑stable | damps stiff transients |
gauss2 (Gauss–Legendre) |
2 | 4 | A‑stable | most accurate per stage; symplectic |
lobatto3 |
3 | 4 | A‑stable | Simpson's rule; stiffly accurate |
radau3 |
3 | 5 | L‑stable | robust default when stiff |
gauss3 |
3 | 6 | A‑stable | most accurate per stage; symplectic |
lobatto4 |
4 | 6 | A‑stable | stiffly accurate |
radau4 |
4 | 7 | L‑stable | highest order with L‑stability |
gauss4 |
4 | 8 | A‑stable | highest order shipped; symplectic |
Prefer Gauss for accuracy on smooth problems and Radau IIA when the operator is stiff — A‑stability alone does not damp the stiffest modes, which is why Radau IIA remains the robust choice despite the lower order.
Because the stage order equals the stage count and caps the objective, moving from q=2 to q=3 raises the ceiling for every family, not just the classical order.
Switch via the method argument in train_heat_equation.
Extending the library
- Add RK variants. Pass the nodes to
collocation_tableau(c)and it derivesAandbfor you — for a collocation method they are forced by the nodes. Gauss, Radau IIA and Lobatto IIIA ship at q=2, 3 and 4. No other code changes required. - New PDE operators. Create a class implementing the
EllipticOperatorprotocol and supply it toRkPinnLoss. - Initial/boundary data. Replace
init_dataand/or change the boundary factor $\Phi$ for different domains/BCs. - Right‑hand side. Provide a custom
f_rhs(x,t)callable.
Reproducibility & testing
- Unit tests live in
tests/and cover training sanity and error bounds. - Set environment variables as needed for deterministic PyTorch runs (note some CUDA ops are non‑deterministic).
- CI runs
ruff,mypy, andpyteston Linux, macOS, and Windows across multiple Python versions.
Examples and notebooks
Runnable scripts live in examples/, and two notebooks in
examples/notebooks/ are committed with their outputs, so they
render on GitHub without being run:
visualization.ipynb— training history, solution evolution against the exact solution, a space‑time error map, an interactive time slider, andresidual="rk"vs"interpolant"on one seed.benchmarking.ipynb— cost per step, scaling inNandn_x_train, memory, CPU/GPU, and the accuracy‑per‑unit‑cost study reproducing the convergence orders above.
poetry install --with examples
poetry run jupyter lab examples/notebooks
Every figure and number in them was produced by executing the notebook against the
current code. tests/test_notebooks.py asserts they parse, store no error outputs, and
were genuinely executed rather than authored by hand.
Benchmarks (indicative)
Training the heat‑equation example for ~1k–5k steps typically reaches $L^2$ errors between 1e-2 and 1e-1 at T=0.1, depending on the RK scheme and batch sizes. Use radau2 for stability and increase N and training steps for tighter accuracy.
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Key points:
- Follow code style (ruff, mypy)
- Add tests for new features
- Update documentation and CHANGELOG
- Use conventional commit messages
Citation
If you use this software in your research, please cite it:
@software{ribeiro_pinn_rk,
author = {Ribeiro, Diogo},
title = {pinn-rk: Runge-Kutta Physics-Informed Neural Networks},
year = {2026},
publisher = {Zenodo},
doi = {10.5281/zenodo.21839391},
url = {https://doi.org/10.5281/zenodo.21839391},
version = {0.2.0}
}
The DOI above is the concept DOI: it always resolves to the latest version. To cite a specific release, use its own DOI instead — 10.5281/zenodo.21875876 for v0.6.0, 10.5281/zenodo.21871378 for v0.5.1, 10.5281/zenodo.21865023 for v0.5.0, 10.5281/zenodo.21860298 for v0.4.0, 10.5281/zenodo.21850047 for v0.3.0, 10.5281/zenodo.21843920 for v0.2.0, 10.5281/zenodo.21839392 for v0.1.0.
Or see CITATION.cff for the full citation information.
License
This project is licensed under the MIT License. See LICENSE for details.
Roadmap
See ROADMAP.md for planned features including:
- Higher-order RK methods (q≥3)
- 2D/3D operators
- Adaptive time stepping
- Additional boundary conditions
- Documentation website
Acknowledgments
This work builds upon research in Physics-Informed Neural Networks and time-stepping methods for PDEs.
Support
- 📖 Documentation
- 🐛 Issue Tracker
- 💬 Discussions
- 📧 Contact: dfr@esmad.ipp.pt
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pinn_rk-0.6.0.tar.gz.
File metadata
- Download URL: pinn_rk-0.6.0.tar.gz
- Upload date:
- Size: 28.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb61cba52b03b5781d2665d7cf86f4ec911d76777d7e916b19f7aec25bd47b16
|
|
| MD5 |
9205dd34f3781ca0311dc6ddaab1ba7d
|
|
| BLAKE2b-256 |
0782e0a689c9f31847d55bb7eb1012d88b92254143abaab488c4479051f5b3d3
|
Provenance
The following attestation bundles were made for pinn_rk-0.6.0.tar.gz:
Publisher:
publish-pypi.yml on DiogoRibeiro7/pinn-rk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pinn_rk-0.6.0.tar.gz -
Subject digest:
eb61cba52b03b5781d2665d7cf86f4ec911d76777d7e916b19f7aec25bd47b16 - Sigstore transparency entry: 2410383541
- Sigstore integration time:
-
Permalink:
DiogoRibeiro7/pinn-rk@f401d404ffdd0ce67b9bef16abd0e35f0294a838 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/DiogoRibeiro7
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f401d404ffdd0ce67b9bef16abd0e35f0294a838 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file pinn_rk-0.6.0-py3-none-any.whl.
File metadata
- Download URL: pinn_rk-0.6.0-py3-none-any.whl
- Upload date:
- Size: 26.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b553c94cb976c61c16a3bdcfa97e792b081e8a10538685e7f17929f9d31c7d1
|
|
| MD5 |
161cc0b048fe668721ffdb863f0bd5e8
|
|
| BLAKE2b-256 |
7526fb86b46b1570161d7bce3f34bafbf0d6cfd06304a415421913b70014df67
|
Provenance
The following attestation bundles were made for pinn_rk-0.6.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on DiogoRibeiro7/pinn-rk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pinn_rk-0.6.0-py3-none-any.whl -
Subject digest:
5b553c94cb976c61c16a3bdcfa97e792b081e8a10538685e7f17929f9d31c7d1 - Sigstore transparency entry: 2410383605
- Sigstore integration time:
-
Permalink:
DiogoRibeiro7/pinn-rk@f401d404ffdd0ce67b9bef16abd0e35f0294a838 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/DiogoRibeiro7
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f401d404ffdd0ce67b9bef16abd0e35f0294a838 -
Trigger Event:
workflow_dispatch
-
Statement type: