-
Notifications
You must be signed in to change notification settings - Fork 959
Add inverse Klein-Gordon example (mass parameter recovery) #2065
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
76616ff
fe0f423
9141e5a
59d5ee3
6b16cf0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| 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. | ||
|
|
||
| The learnable parameter :math:`m^2` is initialized far from its true value of 4 so that recovery is non-trivial: | ||
|
|
||
| .. 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 as random ``(x, t)`` coordinate pairs in the interior. Each row of ``observe_x`` is ``[x_i, t_i]``. The ``component=0`` argument specifies that the first (and only) network output must match the observations — for multi-output networks this selects which output is constrained: | ||
|
|
||
| .. 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 logs the current value of :math:`m^2` every 1,000 iterations to ``variables.dat``, letting us track how the parameter converges over training: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| model.compile( | ||
| "adam", | ||
| lr=0.001, | ||
| metrics=["l2 relative error"], | ||
| external_trainable_variables=m_sq, | ||
| ) | ||
| # Logs m_sq every 1000 iterations so we can plot its convergence trajectory | ||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| """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 observations: 50 random interior (x, t) coordinate pairs. | ||
| # Each row of observe_x is [x_i, t_i]; the network learns to fit these. | ||
| 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) | ||
| # component=0: the first (and only) network output must match observations. | ||
| # For multi-output networks, this selects which output is constrained. | ||
| ptset = dde.icbc.PointSetBC(observe_x, observe_y, component=0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| ) | ||
| # VariableValue callback: logs m_sq to "variables.dat" every 1000 iterations, | ||
| # so we can plot the convergence trajectory of the recovered parameter. | ||
| variable = dde.callbacks.VariableValue(m_sq, period=1000, filename="variables.dat") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment.
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.