1. Unknown coefficient
\(-\Delta u + c(x)\, u = f\)
The problem is posed on a disk, where u is
observed at a few points and c is
unknown. Compared to the forward problem, the only
change is to replace the fixed coefficient with a small network
wrapped in an ApproximationSpace. The forward FEM
solve, the mesh and the basis stay the same.
import jax
from scimba_jax.linear_approximation.solvers import LinearSolve
from scimba_jax.nonlinear_approximation.approximation_spaces.approximation_spaces import (
ApproximationSpace,
)
from scimba_jax.nonlinear_approximation.approximation_spaces.dg_approximation_spaces import (
DGEllipticApproximationSpace,
)
from scimba_jax.nonlinear_approximation.networks.mlp import MLP
from scimba_jax.physical_models.classical_weakform.laplacian_inverse_linear_problem import (
LaplacianReactionWeakFormLearnableCoeff,
)
# u observed at a few points, c unknown (mesh, model and variables from the FEM page).
# c(x) = softplus(MLP(x)) is an ApproximationSpace, and softplus keeps c positive.
def positive(*args):
return jax.nn.softplus(args[0])
coefficient = ApproximationSpace(
{"x": 2},
[(MLP(in_size=2, out_size=1, hidden_sizes=[16, 16], key=key), "scalar", None)],
post_processing=positive,
)
model.add_weak_form(
"main", LaplacianReactionWeakFormLearnableCoeff(dim=2, c=coefficient, f=f_source)
)
# The forward solve is the same linear system as on the FEM page.
scheme = EllipticFEscheme(model, variables)
space = DGEllipticApproximationSpace(
dims={"x": 2, "dofsl": 1},
list_assemblers=[scheme],
model_type="x_dofsl",
newton_kwargs={"solver": LinearSolve(tol=1e-7)},
)
# 89 DOFs, 40 observations, 20 ENG epochs: loss 5.7e-07, relative L2 error
# on c of 8.0e-02, against 0.36 for the best constant c.
2. Data loss and natural gradient
Here, the only loss of the Projector compares the
output of the solve with the observations, and there is no PDE
residual. The forward solve is computed inside this loss,
so the optimization differentiates through the linear solve. The
map from c to u is smoothing, so plain
gradient descent performs poorly. The natural gradient optimizer
(ENG, see PINNs) rescales
the gradient with the metric of the parametrization, and it is
needed in practice for this class of problems to converge.
from scimba_jax.nonlinear_approximation.integration.monte_carlo import (
DomainSampler,
TensorizedSampler,
)
from scimba_jax.nonlinear_approximation.numerical_solvers.projectors import Projector
from scimba_jax.physical_models.abstract_physical_model import AbstractPhysicalModel
from scimba_jax.physical_models.data_residuals import CollocDataResidual
# Data loss: compares u_h with the measurements at the observation points.
class DataFittingModel(AbstractPhysicalModel):
def __init__(self, domain, data):
super().__init__(main_domain=domain)
self.data_residuals["data"] = CollocDataResidual(
size=1, model_type="x_dofsl", data=data
)
model = DataFittingModel(domain, data=(points, observations))
sampler = TensorizedSampler(
[DomainSampler(domain)], bc=False, data_samplers=model.data_residuals
)
# ENG is needed here: c -> u is smoothing, so with plain gradient descent,
# the loss decreases while c stays far from the exact coefficient.
projector = Projector(model, space, sampler, optimizer="ENG", matrix_regularization=1e-6)
key, projector = projector.project(key, space, n_epochs=20, n_dl=len(points))
# `matrix_regularization` is required: the Gram matrix is rank deficient (some
# directions are not seen by the data), so Cholesky gives nan without it.
The same approach applies to the other discretizations (DG, finite volumes or a collocation basis), and to several coefficients at once, as long as each coefficient is wrapped in the same way and can be identified from the available data.