Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/demos/pinn_inverse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Time-dependent PDEs
:maxdepth: 1

pinn_inverse/diffusion.1d.inverse
pinn_inverse/klein.gordon.inverse
pinn_inverse/reaction.inverse

- `Inverse problem for the Navier-Stokes equation of incompressible flow around cylinder <https://github.com/lululxvi/deepxde/blob/master/examples/pinn_inverse/Navier_Stokes_inverse.py>`_
Expand Down
158 changes: 158 additions & 0 deletions docs/demos/pinn_inverse/klein.gordon.inverse.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
Inverse problem for the Klein-Gordon equation
================

Problem setup
--------------

We will solve an inverse problem for the Klein-Gordon equation to recover the unknown mass parameter :math:`m^2`:

.. math:: \frac{\partial^2 u}{\partial t^2} - \frac{\partial^2 u}{\partial x^2} + m^2 u = 0

for :math:`x \in [-1, 1]` and :math:`t \in [0, 1]` with Dirichlet boundary conditions

.. math:: u(-1, t) = u_{\text{exact}}(-1, t), \quad u(1, t) = u_{\text{exact}}(1, t)

and initial conditions

.. math:: u(x, 0) = \sin(\pi x), \quad \frac{\partial u}{\partial t}(x, 0) = 0.

The exact solution is :math:`u(x, t) = \sin(\pi x) \cos(\omega t)` where :math:`\omega = \sqrt{\pi^2 + m^2}`.

The true mass parameter is :math:`m^2 = 4`. We initialize at :math:`m^2 = 1` and recover the true value using 50 randomly sampled observation points in the interior of the domain.

Implementation
--------------

This description goes through the implementation of a solver for the above described inverse Klein-Gordon problem step-by-step.

First, the DeepXDE and NumPy modules are imported:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

28-33 is not useful information.


.. code-block:: python

import deepxde as dde
import numpy as np

We define the true mass parameter :math:`m^2 = 4` and the corresponding frequency :math:`\omega`:

.. code-block:: python

m_sq_true = 4.0
omega = np.sqrt(np.pi**2 + m_sq_true)

The learnable parameter :math:`m^2` is initialized far from its true value:

.. code-block:: python

m_sq = dde.Variable(1.0)

We define the PDE residual :math:`u_{tt} - u_{xx} + m^2 u = 0`:

.. code-block:: python

def pde(x, y):
dy_tt = dde.grad.hessian(y, x, i=1, j=1)
dy_xx = dde.grad.hessian(y, x, i=0, j=0)
return dy_tt - dy_xx + m_sq * y

The exact solution ``func`` is defined as:

.. code-block:: python

def func(x):
return np.sin(np.pi * x[:, 0:1]) * np.cos(omega * x[:, 1:2])

We set up the computational domain as a 1D interval crossed with a time domain:

.. code-block:: python

geom = dde.geometry.Interval(-1, 1)
timedomain = dde.geometry.TimeDomain(0, 1)
geomtime = dde.geometry.GeometryXTime(geom, timedomain)

The boundary and initial conditions are specified. We use Dirichlet BCs on the full boundary, and two initial conditions — one for the field value and one for the time derivative:

.. code-block:: python

bc = dde.icbc.DirichletBC(geomtime, func, lambda _, on_boundary: on_boundary)
ic_1 = dde.icbc.IC(geomtime, func, lambda _, on_initial: on_initial)
ic_2 = dde.icbc.OperatorBC(
geomtime,
lambda x, y, _: dde.grad.jacobian(y, x, i=0, j=1),
lambda _, on_initial: on_initial,
)

We create 50 sparse observation points in the interior to provide data for parameter recovery:

.. code-block:: python

rng = np.random.default_rng(42)
observe_x = np.column_stack(
[rng.uniform(-1, 1, 50), rng.uniform(0, 1, 50)]
)
observe_y = func(observe_x)
ptset = dde.icbc.PointSetBC(observe_x, observe_y, component=0)

The PDE problem is assembled with 2000 domain points, 200 boundary points, and 200 initial condition points:

.. code-block:: python

data = dde.data.TimePDE(
geomtime,
pde,
[bc, ic_1, ic_2, ptset],
num_domain=2000,
num_boundary=200,
num_initial=200,
anchors=observe_x,
solution=func,
num_test=5000,
)

We use a fully connected network with 3 hidden layers of width 40:

.. code-block:: python

net = dde.nn.FNN([2] + [40] * 3 + [1], "tanh", "Glorot uniform")
model = dde.Model(data, net)

Phase 1 trains with Adam for 30,000 iterations. The ``VariableValue`` callback tracks the evolution of :math:`m^2`:

.. code-block:: python

model.compile(
"adam",
lr=0.001,
metrics=["l2 relative error"],
external_trainable_variables=m_sq,
)
variable = dde.callbacks.VariableValue(m_sq, period=1000, filename="variables.dat")
losshistory, train_state = model.train(iterations=30000, callbacks=[variable])

Phase 2 refines with L-BFGS for further convergence:

.. code-block:: python

model.compile(
"L-BFGS",
metrics=["l2 relative error"],
external_trainable_variables=m_sq,
)
losshistory, train_state = model.train(callbacks=[variable])

dde.saveplot(losshistory, train_state, issave=True, isplot=True)

Training results
-----------------

After 30,000 Adam iterations, :math:`m^2` converges from 1.0 to approximately 2.0 with an L2 relative error of ~4.4%. The subsequent L-BFGS phase (~5,900 additional iterations) drives :math:`m^2` to **4.00** (the exact true value) with a final L2 relative error of **0.047%**.

Total runtime is approximately 8 minutes on a GPU (NVIDIA RTX 4060, PyTorch backend):

- Adam (30k iterations): ~290 s
- L-BFGS (~5.9k iterations): ~180 s

Complete code
--------------

.. literalinclude:: ../../../examples/pinn_inverse/Klein_Gordon_inverse.py
:language: python
91 changes: 91 additions & 0 deletions examples/pinn_inverse/Klein_Gordon_inverse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Backend supported: tensorflow.compat.v1, tensorflow, pytorch, paddle

Inverse problem for the Klein-Gordon equation: recover the mass parameter m^2
from sparse observations of the solution.

The Klein-Gordon equation is:
u_tt - u_xx + m^2 u = 0

with exact solution u(x,t) = sin(pi*x) cos(omega*t), omega^2 = pi^2 + m^2.
The true mass parameter is m^2 = 4. We initialize at m^2 = 1 and recover it
from 50 randomly sampled observation points.
"""
import deepxde as dde
import numpy as np

# True mass parameter: m^2 = 4 (to be recovered)
m_sq_true = 4.0
omega = np.sqrt(np.pi**2 + m_sq_true)

# Learnable parameter, initialized far from true value
m_sq = dde.Variable(1.0)


def pde(x, y):
"""Klein-Gordon residual: u_tt - u_xx + m^2 u = 0."""
dy_tt = dde.grad.hessian(y, x, i=1, j=1)
dy_xx = dde.grad.hessian(y, x, i=0, j=0)
return dy_tt - dy_xx + m_sq * y


def func(x):
"""Exact solution: u(x,t) = sin(pi*x) cos(omega*t)."""
return np.sin(np.pi * x[:, 0:1]) * np.cos(omega * x[:, 1:2])


geom = dde.geometry.Interval(-1, 1)
timedomain = dde.geometry.TimeDomain(0, 1)
geomtime = dde.geometry.GeometryXTime(geom, timedomain)

# Boundary and initial conditions
bc = dde.icbc.DirichletBC(geomtime, func, lambda _, on_boundary: on_boundary)
ic_1 = dde.icbc.IC(geomtime, func, lambda _, on_initial: on_initial)
# Velocity IC: du/dt(x,0) = 0
ic_2 = dde.icbc.OperatorBC(
geomtime,
lambda x, y, _: dde.grad.jacobian(y, x, i=0, j=1),
lambda _, on_initial: on_initial,
)

# Sparse observation data at 50 random interior points
rng = np.random.default_rng(42)
observe_x = np.column_stack(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's important to mention here (in a comment) that these are (x, t) pairs, even though this is inferrable. Keep in mind that the audience of examples are new to deepXDE, and this clarification justifies the logic in all the indexing.

[rng.uniform(-1, 1, 50), rng.uniform(0, 1, 50)]
)
observe_y = func(observe_x)
ptset = dde.icbc.PointSetBC(observe_x, observe_y, component=0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same logic here: mention that component parameter specifies which output needs to satisfy the BC. (In this case, there is only one).


data = dde.data.TimePDE(
geomtime,
pde,
[bc, ic_1, ic_2, ptset],
num_domain=2000,
num_boundary=200,
num_initial=200,
anchors=observe_x,
solution=func,
num_test=5000,
)

net = dde.nn.FNN([2] + [40] * 3 + [1], "tanh", "Glorot uniform")
model = dde.Model(data, net)

# Phase 1: Adam optimization
model.compile(
"adam",
lr=0.001,
metrics=["l2 relative error"],
external_trainable_variables=m_sq,
)
variable = dde.callbacks.VariableValue(m_sq, period=1000, filename="variables.dat")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optionally explain this callback structure more (you already did in the docs)

losshistory, train_state = model.train(iterations=30000, callbacks=[variable])

# Phase 2: L-BFGS refinement
model.compile(
"L-BFGS",
metrics=["l2 relative error"],
external_trainable_variables=m_sq,
)
losshistory, train_state = model.train(callbacks=[variable])

dde.saveplot(losshistory, train_state, issave=True, isplot=True)