A simple ODE system
Problem setup
We will solve a simple ODE system:
with the initial conditions
The reference solution is \(y_1 = \sin(t), \quad y_2 = \cos(t)\).
Implementation
This description goes through the implementation of a solver for the above ODE system step-by-step.
First, the DeepXDE and NumPy (np
) modules are imported:
import deepxde as dde
import numpy as np
We begin by defining a computational geometry. We can use a built-in class TimeDomain
to define a time domain as follows
geom = dde.geometry.TimeDomain(0, 10)
Next, we express the ODE system:
def ode_system(x, y):
y1, y2 = y[:, 0:1], y[:, 1:]
dy1_x = dde.grad.jacobian(y, x, i=0)
dy2_x = dde.grad.jacobian(y, x, i=1)
return [dy1_x - y2, dy2_x + y1]
The first argument to ode_system
is the network input, i.e., the \(t\)-coordinate, and here we represent it as x
. The second argument to ode_system
is the network output, which is a 2-dimensional vector where the first component(y[:, 0:1]
) is \(y_1\) and the second component (y[:, 1:]
) is \(y_2\).
Next, we consider the initial condition. We need to implement a function, which should return True
for points inside the subdomain and False
for the points outside. In our case, the point \(t\) of the initial condition is \(t = 0\). (Note that because of rounding-off errors, it is often wise to use dde.utils.isclose
to test whether two floating point values are equivalent.)
def boundary(x, on_initial):
return dde.utils.isclose(x[0], 0)
The argument x
to boundary
is the network input and is a \(d\)-dim vector, where \(d\) is the dimension and \(d=1\) in this case. To facilitate the implementation of boundary
, a boolean on_initial
is used as the second argument. If the point \(t = 0\), then on_initial
is True, otherwise, on_initial
is False. Thus, we can also define boundary
in a simpler way:
def boundary(_, on_initial):
return on_initial
Then the initial conditions are specified using the computational domain, initial function and boundary. The argument component
refers to if this IC is for the first component or the second component.
ic1 = dde.icbc.IC(geom, lambda x: 0, boundary, component=0)
ic2 = dde.icbc.IC(geom, lambda x: 1, boundary, component=1)
Now, we have specified the geometry, ODEs, and initial conditions. Since PDE
is also an ODE solver, we then define the ODE problem as
data = dde.data.PDE(geom, ode_system, [ic1, ic2], 35, 2, solution=func, num_test=100)
The number 35 is the number of training residual points sampled inside the domain, and the number 2 is the number of training points sampled on the boundary (the left and right endpoints of the time domain in this case). We use 100 points for testing the ODE residual. The argument solution=func
is the reference solution to compute the error of our solution, and we define it as follows:
def func(x):
return np.hstack((np.sin(x), np.cos(x)))
Next, we choose the network. Here, we use a fully connected neural network of depth 4 (i.e., 3 hidden layers) and width 50:
layer_size = [1] + [50] * 3 + [2]
activation = "tanh"
initializer = "Glorot uniform"
net = dde.nn.FNN(layer_size, activation, initializer)
Now, we have the ODE problem and the network. We bulid a Model
and choose the optimizer and learning rate:
model = dde.Model(data, net)
model.compile("adam", lr=0.001, metrics=["l2 relative error"])
We then train the model for 20000 iterations:
losshistory, train_state = model.train(iterations=20000)
We also save and plot the best trained result and loss history.
dde.saveplot(losshistory, train_state, issave=True, isplot=True)
Complete code
"""Backend supported: tensorflow.compat.v1, tensorflow, pytorch, jax, paddle"""
import deepxde as dde
import numpy as np
def ode_system(x, y):
"""ODE system.
dy1/dx = y2
dy2/dx = -y1
"""
# Most backends
y1, y2 = y[:, 0:1], y[:, 1:]
dy1_x = dde.grad.jacobian(y, x, i=0)
dy2_x = dde.grad.jacobian(y, x, i=1)
# Backend jax
# y_val, y_fn = y
# y1, y2 = y_val[:, 0:1], y_val[:, 1:]
# dy1_x, _ = dde.grad.jacobian(y, x, i=0)
# dy2_x, _ = dde.grad.jacobian(y, x, i=1)
return [dy1_x - y2, dy2_x + y1]
def boundary(_, on_initial):
return on_initial
def func(x):
"""
y1 = sin(x)
y2 = cos(x)
"""
return np.hstack((np.sin(x), np.cos(x)))
geom = dde.geometry.TimeDomain(0, 10)
ic1 = dde.icbc.IC(geom, lambda x: 0, boundary, component=0)
ic2 = dde.icbc.IC(geom, lambda x: 1, boundary, component=1)
data = dde.data.PDE(geom, ode_system, [ic1, ic2], 35, 2, solution=func, num_test=100)
layer_size = [1] + [50] * 3 + [2]
activation = "tanh"
initializer = "Glorot uniform"
net = dde.nn.FNN(layer_size, activation, initializer)
model = dde.Model(data, net)
model.compile("adam", lr=0.001, metrics=["l2 relative error"])
losshistory, train_state = model.train(iterations=20000)
dde.saveplot(losshistory, train_state, issave=True, isplot=True)