Skip to content

Inverse problems + hybrid physics–data¤

This recipe illustrates a common “SciML inverse” pattern: learn a state \(u\) and an unknown coefficient/parameter using a PDE residual plus data terms.

Problem (example)¤

On a spatial domain \(\Omega\), consider

\[ -\nabla\cdot\bigl(k(x)\nabla u(x)\bigr)=f(x)\quad\text{in }\Omega,\qquad u=g\quad\text{on }\partial\Omega, \]

where \(k\) is unknown (either a scalar parameter or a field), and you also have sparse observations of \(u\).

Pattern: treat unknowns as additional fields¤

In Phydrax, you typically represent unknowns as additional DomainFunctions and couple them inside the residual operator. Everything is still “minimize functionals over domains”.

Example skeleton (learn \(u_\theta\) and \(k_\phi\))¤

Example

import jax
import jax.numpy as jnp
import jax.random as jr
import optax
import phydrax as phx

geom = phx.domain.Square(center=(0.0, 0.0), side=2.0)  # Ω=[-1,1]^2

# Known forcing and boundary value (toy choices)
@geom.Function("x")
def f(x):
    return 1.0

@geom.Function("x")
def g(x):
    return 0.0

# State u(x) and unknown coefficient k(x) (positive via final activation)
u_model = phx.nn.MLP(in_size=2, out_size="scalar", width_size=16, depth=2, key=jr.key(0))
k_model = phx.nn.MLP(
    in_size=2,
    out_size="scalar",
    width_size=16,
    depth=2,
    final_activation=jax.nn.softplus,
    key=jr.key(1),
)

u = geom.Model("x")(u_model)
k = geom.Model("x")(k_model)

structure = phx.domain.ProductStructure((("x",),))

def pde_operator(u_f, k_f):
    grad_u = phx.operators.grad(u_f, var="x")      # ∇u (vector)
    flux = k_f * grad_u                            # k∇u
    return -phx.operators.div(flux, var="x") - f    # -∇·(k∇u) - f

pde = phx.constraints.ContinuousPointwiseInteriorConstraint(
    ("u", "k"),
    geom,
    operator=pde_operator,
    num_points=128,
    structure=structure,
    reduction="mean",
)

boundary = geom.component({"x": phx.domain.Boundary()})
bc = phx.constraints.ContinuousDirichletBoundaryConstraint(
    "u",
    boundary,
    target=g,
    num_points=64,
    structure=structure,
    weight=10.0,
)

# Optional anchor data for u at scattered points
anchors = jnp.array([[0.0, 0.0], [0.5, -0.25]])
values = jnp.array([0.0, 0.1])
data = phx.constraints.DiscreteInteriorDataConstraint(
    "u",
    geom,
    points={"x": anchors},
    values=values,
    weight=1.0,
)

solver = phx.solver.FunctionalSolver(functions={"u": u, "k": k}, constraints=[pde, bc, data])
solver = solver.solve(num_iter=20, optim=optax.adam(1e-3), seed=0)

Notes¤

  • For scalar unknown parameters (global coefficients), consider domain.Parameter(...) for a trainable constant.
  • If you want to enforce \(u=g\) exactly, replace the boundary penalty with an enforced term (see the Poisson recipe).
  • For sensor tracks over time, use DiscreteInteriorDataConstraint(..., sensors=..., times=..., sensor_values=...).