| Title: | Physics-Informed Neural Networks in Native R with 'torch' |
|---|---|
| Description: | Fits physics-informed neural networks (PINNs) that solve ordinary and partial differential equations and estimate unknown model parameters from data. Networks are trained by penalising the equation residual, boundary and initial conditions, and misfit to observations, following Raissi, Perdikaris and Karniadakis (2019) <doi:10.1016/j.jcp.2018.10.045>. Everything runs in native R on top of 'torch', with no 'Python' dependency, and ordinary differential equation models written as 'deSolve'-style derivative functions can be used directly. |
| Authors: | Samson Obasanmi [aut, cre] (ORCID: <https://orcid.org/0009-0009-6935-5479>) |
| Maintainer: | Samson Obasanmi <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.1 |
| Built: | 2026-11-01 23:37:33 UTC |
| Source: | https://github.com/Obasanmii/pinnr |
Estimated parameters
## S3 method for class 'pinn_fit' coef(object, ...)## S3 method for class 'pinn_fit' coef(object, ...)
object |
A |
... |
Unused. |
Named numeric vector of estimated unknown parameters (empty if the problem had none).
Conditions are enforced as soft penalties on points sampled where they
apply. Each condition fixes one or more domain variables (where) and
samples the remaining variables over their full range.
pinn_dirichlet( where, value = 0, output = NULL, n = 100, weight = 1, name = NULL ) pinn_neumann( where, along, value = 0, output = NULL, n = 100, weight = 1, name = NULL ) pinn_initial( value, time = "t", output = NULL, n = 100, weight = 1, name = NULL ) pinn_condition(where, fn, n = 100, weight = 1, name = NULL)pinn_dirichlet( where, value = 0, output = NULL, n = 100, weight = 1, name = NULL ) pinn_neumann( where, along, value = 0, output = NULL, n = 100, weight = 1, name = NULL ) pinn_initial( value, time = "t", output = NULL, n = 100, weight = 1, name = NULL ) pinn_condition(where, fn, n = 100, weight = 1, name = NULL)
where |
Named list giving the fixed value(s) of one or more domain variables. See Locations. |
value |
Target value. See Values. |
output |
Name(s) of the output(s) the condition applies to. The
default |
n |
Number of points used for this condition. |
weight |
Relative weight of this condition in the loss. |
name |
Optional label used in printed output. |
along |
Name of the domain variable to differentiate with respect to. |
time |
Name of the time variable for |
fn |
For |
pinn_dirichlet() fixes the value of the solution, .
pinn_neumann() fixes a first derivative, .
pinn_initial() is a Dirichlet condition at the lower end of the time
variable.
pinn_condition() takes any residual function, for Robin, periodic
or other conditions.
An object of class pinn_condition.
Each element of where is a number or one of the
strings "lower", "upper" or "both", which refer to the ends of
that variable's range. where = list(x = "both") places points on both
ends of x. When several variables are fixed, every combination of
their values is used: where = list(x = "both", y = "both") places
points on the four corners.
value can be a number, a numeric vector named by
output, or a function. A function's arguments are matched by name to
the domain variables, outputs and parameters, so
value = function(x) sin(pi * x) uses the spatial variable x.
Inside these functions the arguments are torch tensors, so use
arithmetic and maths functions (sin, exp, log, ...) rather than
control flow such as if or ifelse.
# u = 0 on both ends of x pinn_dirichlet(where = list(x = "both"), value = 0) # u(x, 0) = sin(pi x) pinn_initial(function(x) sin(pi * x), time = "t") # insulated right boundary: du/dx = 0 at x = 1 pinn_neumann(where = list(x = "upper"), along = "x") # Robin condition du/dx + 2 u = 0 at x = 1, as a custom residual pinn_condition(where = list(x = "upper"), fn = function(u, x, d) d(u, x) + 2 * u)# u = 0 on both ends of x pinn_dirichlet(where = list(x = "both"), value = 0) # u(x, 0) = sin(pi x) pinn_initial(function(x) sin(pi * x), time = "t") # insulated right boundary: du/dx = 0 at x = 1 pinn_neumann(where = list(x = "upper"), along = "x") # Robin condition du/dx + 2 u = 0 at x = 1, as a custom residual pinn_condition(where = list(x = "upper"), fn = function(u, x, d) d(u, x) + 2 * u)
Each argument names an input variable of the problem and gives its range
as c(lower, upper). The order of the arguments is the order in which
the variables enter the network.
pinn_domain(...)pinn_domain(...)
... |
Named numeric vectors of length 2, for example
|
An object of class pinn_domain.
pinn_domain(x = c(-1, 1), t = c(0, 1))pinn_domain(x = c(-1, 1), t = c(0, 1))
Fits a fully connected network to a pinn_problem() (or a problem from
pinn_ode()) by minimising
where each term is a mean squared error. Training runs epochs steps of
Adam and then, optionally, lbfgs_iter iterations of L-BFGS, which
often sharpens the solution considerably.
pinn_fit( problem, hidden = c(32, 32, 32), activation = "tanh", epochs = 5000, lr = 0.001, lr_unknown = NULL, lbfgs_iter = 0, n_collocation = 1000, sampling = c("lhs", "uniform"), resample_every = 0, weights = c(residual = 1, conditions = 1, data = 1), precision = c("float32", "float64"), device = "cpu", seed = NULL, verbose = interactive(), record_every = 10, windows = 1 )pinn_fit( problem, hidden = c(32, 32, 32), activation = "tanh", epochs = 5000, lr = 0.001, lr_unknown = NULL, lbfgs_iter = 0, n_collocation = 1000, sampling = c("lhs", "uniform"), resample_every = 0, weights = c(residual = 1, conditions = 1, data = 1), precision = c("float32", "float64"), device = "cpu", seed = NULL, verbose = interactive(), record_every = 10, windows = 1 )
problem |
A |
|
Integer vector of hidden layer widths. |
|
activation |
Activation function; see |
epochs |
Number of Adam steps. |
lr |
Adam learning rate for the network weights. |
lr_unknown |
Adam learning rate for unknown parameters. Defaults to
|
lbfgs_iter |
Number of L-BFGS iterations after Adam (0 to skip). |
n_collocation |
Number of points inside the domain where the residual is evaluated. |
sampling |
|
resample_every |
Draw new collocation and condition points every this many Adam steps (0 keeps one fixed set). Resampling reduces overfitting to the training points. |
weights |
Named numeric vector with loss weights |
precision |
|
device |
A torch device, such as |
seed |
Optional integer seed for reproducible sampling and weights. |
verbose |
Print progress messages. |
record_every |
Store the loss every this many steps. |
windows |
Number of time windows for ODE problems; see Long time horizons. |
An object of class pinn_fit, with methods for predict(),
coef(), residuals(), plot() and summary().
The defaults solve small problems on a CPU
in seconds to minutes. If the loss stalls, try more epochs, a wider or
deeper network, more collocation points, or lbfgs_iter = 500. For
parameter estimation, a larger lr_unknown lets the parameters move
faster than the network weights. Long time horizons are hard for
PINNs; shorter windows, or more data, help.
PINNs often converge to a wrong, flat
solution when an ODE is solved over a long horizon with fast early
dynamics (for example an epidemic starting from a few cases). For
problems from pinn_ode(), windows = k splits the time range into
k equal windows and trains one network per window, each starting
from the end state of the previous one ("time marching"). This is
available for forward problems (no unknown parameters).
Pressing Esc (or Ctrl+C) stops training early and returns the model as
trained so far, with a warning. If the loss becomes NaN or infinite,
training stops and the weights with the lowest loss seen so far are
returned, also with a warning (and diverged = TRUE in the result).
# Exponential decay dy/dt = -k y decay <- function(t, y, parms) list(-parms["k"] * y) prob <- pinn_ode(decay, y0 = c(y = 1), times = seq(0, 5, by = 0.5), parms = c(k = 0.5)) fit <- pinn_fit(prob, hidden = c(16, 16), epochs = 1500, seed = 1) head(predict(fit))# Exponential decay dy/dt = -k y decay <- function(t, y, parms) list(-parms["k"] * y) prob <- pinn_ode(decay, y0 = c(y = 1), times = seq(0, 5, by = 0.5), parms = c(k = 0.5)) fit <- pinn_fit(prob, hidden = c(16, 16), epochs = 1500, seed = 1) head(predict(fit))
A multilayer perceptron with Xavier-normal initial weights and zero biases, the standard choice for PINNs.
pinn_mlp(n_in, hidden = c(32, 32, 32), n_out = 1, activation = "tanh")pinn_mlp(n_in, hidden = c(32, 32, 32), n_out = 1, activation = "tanh")
n_in |
Number of inputs. |
|
Integer vector of hidden layer widths. |
|
n_out |
Number of outputs. |
activation |
|
A torch::nn_module.
net <- pinn_mlp(2, c(16, 16), 1) net(torch::torch_randn(5, 2))net <- pinn_mlp(2, c(16, 16), 1) net(torch::torch_randn(5, 2))
Turns an ordinary differential equation written for
deSolve::ode() into a pinn_problem(). The same derivative function
can be used to solve the system (a forward problem) or, with unknown
and data, to estimate its parameters from observations.
pinn_ode( func, y0, times, parms = NULL, unknown = NULL, positive = TRUE, data = NULL, time = "time", hard_initial = TRUE )pinn_ode( func, y0, times, parms = NULL, unknown = NULL, positive = TRUE, data = NULL, time = "time", hard_initial = TRUE )
func |
A derivative function |
y0 |
Named numeric vector of initial states at the first time. |
times |
Numeric vector of times. Only its range is used for
training; the full vector becomes the default grid for |
parms |
Named numeric vector or list of known parameters. |
unknown |
Named numeric vector of initial guesses for parameters to
estimate. These are passed to |
positive |
Logical; see |
data |
Optional data frame of observations with a column named as
|
time |
Name of the time variable. The default, |
hard_initial |
Logical; enforce the initial state exactly. See Initial conditions. |
A pinn_problem() object.
func has the usual deSolve signature
function(t, y, parms) and returns a list whose first element holds
the derivatives, in the order of y0. The recommended style works
unchanged:
function(t, y, parms) {
with(as.list(c(y, parms)), {
dS <- -beta * S * I
...
list(c(dS, dI, dR))
})
}
Indexing with y["S"], y[1] or parms["beta"] also works. Because
the states are torch tensors, the body must use arithmetic and maths
functions; if, ifelse, max and pmin on states do not work.
Forcing functions of time can be written with tensor operations, for
example torch_sin(t) or sin(t).
By default the initial state is enforced
exactly by writing each state as
, where is the
network. This removes one loss term and usually speeds up training.
Set hard_initial = FALSE to use a soft penalty instead.
# Logistic growth dN/dt = r N (1 - N / K) logistic <- function(t, y, parms) { with(as.list(c(y, parms)), list(r * N * (1 - N / K))) } prob <- pinn_ode(logistic, y0 = c(N = 0.1), times = seq(0, 10, by = 0.5), parms = c(r = 1, K = 1)) prob# Logistic growth dN/dt = r N (1 - N / K) logistic <- function(t, y, parms) { with(as.list(c(y, parms)), list(r * N * (1 - N / K))) } prob <- pinn_ode(logistic, y0 = c(N = 0.1), times = seq(0, 10, by = 0.5), parms = c(r = 1, K = 1)) prob
Collects everything a PINN needs: the domain, the names of the unknown functions, the equation residual, conditions, optional observations and the equation's parameters.
pinn_problem( domain, outputs, residual, conditions = list(), data = NULL, parms = list(), unknown = NULL, positive = TRUE, transform = NULL )pinn_problem( domain, outputs, residual, conditions = list(), data = NULL, parms = list(), unknown = NULL, positive = TRUE, transform = NULL )
domain |
A |
outputs |
Character vector naming the unknown functions. |
residual |
Function returning the equation residual. See Writing the residual. |
conditions |
A condition or list of conditions from
|
data |
Optional data frame of observations, with one column per
domain variable and one column for each observed output. Missing
values ( |
parms |
Named list or numeric vector of known constants. |
unknown |
Named numeric vector of initial guesses for parameters to estimate. |
positive |
Logical, recycled or named by |
transform |
Optional output transform. See Output transforms. |
An object of class pinn_problem.
residual is an R function whose
arguments are matched by name to:
the outputs (for example u),
the domain variables (for example x, t),
the parameters in parms and unknown, and
d, the derivative operator.
d(f, x) differentiates f with respect to the variable x;
d(f, x, x) gives the second derivative and d(f, x, t) a mixed
derivative. The function returns the expression that should equal
zero, or a list of such expressions for a system of equations. For
the viscous Burgers equation :
function(u, x, t, nu, d) d(u, t) + u * d(u, x) - nu * d(u, x, x)
All arguments are torch tensors with one row per point, so use
arithmetic and maths functions (sin, exp, log, sqrt, ...)
and avoid if, ifelse or functions that leave R's torch methods.
transform can enforce conditions exactly
("hard constraints"). It is called with the raw network outputs,
domain variables and parameters (matched by name, as above) and
returns a named list of modified outputs. For example, to make
hold exactly:
transform = function(u, x) list(u = x * (1 - x) * u).
pinn_ode() for ordinary differential equations written in
deSolve style, and pinn_fit() to train the network.
# Heat equation u_t = a u_xx with u(x, 0) = sin(pi x) and u = 0 at both ends heat <- pinn_problem( domain = pinn_domain(x = c(0, 1), t = c(0, 1)), outputs = "u", residual = function(u, x, t, a, d) d(u, t) - a * d(u, x, x), conditions = list( pinn_initial(function(x) sin(pi * x), time = "t"), pinn_dirichlet(where = list(x = "both"), value = 0) ), parms = list(a = 0.1) ) heat# Heat equation u_t = a u_xx with u(x, 0) = sin(pi x) and u = 0 at both ends heat <- pinn_problem( domain = pinn_domain(x = c(0, 1), t = c(0, 1)), outputs = "u", residual = function(u, x, t, a, d) d(u, t) - a * d(u, x, x), conditions = list( pinn_initial(function(x) sin(pi * x), time = "t"), pinn_dirichlet(where = list(x = "both"), value = 0) ), parms = list(a = 0.1) ) heat
Draws training points uniformly at random or with Latin hypercube
sampling. Variables listed in fixed are held at the given values, which
is how boundary and initial points are generated.
pinn_sample(n, domain, method = c("lhs", "uniform"), fixed = list())pinn_sample(n, domain, method = c("lhs", "uniform"), fixed = list())
n |
Number of points. |
domain |
|
method |
|
fixed |
Named list of numeric values. When a variable has several
values, the points are split evenly between them. With several fixed
variables, every combination of their values is used, so
|
A data frame with one column per domain variable.
dom <- pinn_domain(x = c(0, 1), t = c(0, 2)) head(pinn_sample(5, dom)) pinn_sample(4, dom, fixed = list(x = c(0, 1)))dom <- pinn_domain(x = c(0, 1), t = c(0, 2)) head(pinn_sample(5, dom)) pinn_sample(4, dom, fixed = list(x = c(0, 1)))
Torch modules cannot be stored with saveRDS() directly. pinn_save()
stores the network weights as plain arrays alongside the rest of the fit,
and pinn_load() rebuilds the network.
pinn_save(fit, path) pinn_load(path)pinn_save(fit, path) pinn_load(path)
fit |
A |
path |
File path, conventionally ending in |
pinn_save() returns path invisibly; pinn_load() returns a
pinn_fit.
Plot a fitted PINN
## S3 method for class 'pinn_fit' plot(x, type = c("loss", "solution", "parameters"), output = NULL, ...)## S3 method for class 'pinn_fit' plot(x, type = c("loss", "solution", "parameters"), output = NULL, ...)
x |
A |
type |
|
output |
For 2D solutions, which output to draw. |
... |
Passed to the underlying base graphics function. |
x, invisibly.
Evaluates the trained network at new points.
## S3 method for class 'pinn_fit' predict(object, newdata = NULL, ...)## S3 method for class 'pinn_fit' predict(object, newdata = NULL, ...)
object |
A |
newdata |
Data frame with one column per domain variable. When
omitted, the |
... |
Unused. |
A data frame with the domain variables followed by one column per output.
decay <- function(t, y, parms) list(-parms["k"] * y) prob <- pinn_ode(decay, y0 = c(y = 1), times = c(0, 5), parms = c(k = 0.5)) fit <- pinn_fit(prob, hidden = c(16, 16), epochs = 1000, seed = 1) predict(fit, data.frame(time = c(0, 1, 2)))decay <- function(t, y, parms) list(-parms["k"] * y) prob <- pinn_ode(decay, y0 = c(y = 1), times = c(0, 5), parms = c(k = 0.5)) fit <- pinn_fit(prob, hidden = c(16, 16), epochs = 1000, seed = 1) predict(fit, data.frame(time = c(0, 1, 2)))
Evaluates the equation residual at new points. Values close to zero mean the network satisfies the equation there; large values show where the solution is unreliable.
## S3 method for class 'pinn_fit' residuals(object, newdata = NULL, ...)## S3 method for class 'pinn_fit' residuals(object, newdata = NULL, ...)
object |
A |
newdata |
Data frame with one column per domain variable. When
omitted, the |
... |
Unused. |
A data frame with the domain variables followed by one residual column per equation.