Inverse problem for the Lorenz system

Problem setup

We will solve the Lorenz system:

\[\frac{dx}{dt} = \sigma(y-x), \quad \frac{dy}{dt} = x (\rho - z) - y, \quad \frac{dz}{dt} = x y - \beta z \qquad t \in [0, 3]\]

with initial conditions

\[x(0) = -8, \quad y(0) = 7, \quad z(0) = 27.\]

The reference solution is here, where the parameters \(\sigma\), \(\rho\), and \(\beta\) are to be identified from observations of the system at certain times and whose true values are 10, 15, and 8/3, respectivly.

Implementation

This description goes through the implementation of a solver for the above Lorenz system step-by-step.

First, the DeepXDE and NumPy (np) modules are imported:

import deepxde as dde
import numpy as np

We also want to define our three unknown variables, \(\sigma\), \(\rho\), and \(\beta\) which will now be called C1, C2, and C3, respectivly. These variables are given an initial guess of 1.0.

C1 = dde.Variable(1.0)
C2 = dde.Variable(1.0)
C3 = dde.Variable(1.0)

Now we can begin by creating a TimeDomain class.

geom = dde.geometry.TimeDomain(0, 3)

Next, we create the Lorenz system to solve using the dde.grad.jacobian function.

def Lorenz_system(x, y):
    y1, y2, y3 = y[:, 0:1], y[:, 1:2], y[:, 2:]
    dy1_x = dde.grad.jacobian(y, x, i=0)
    dy2_x = dde.grad.jacobian(y, x, i=1)
    dy3_x = dde.grad.jacobian(y, x, i=2)
    return [
        dy1_x - C1 * (y2 - y1),
        dy2_x - y1 * (C2 - y3) + y2,
        dy3_x - y1 * y2 + C3 * y3,
    ]

The first argument to Lorenz_system is the network input, i.e., the \(t\)-coordinate. The second argument is the network output, i.e., the solution \(y(x,y,z)\), but here we use y1, y2, y3 as the name of the coordinates x, y, and z, which correspond to the columns of datapoints in the 2D array, \(y\).

Next, we consider the initial conditions. We need to implement a function, which should return True for points inside the subdomain and False for the points outside.

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 (\(x\)), the second component (\(y\)), or the third component (\(z\)). Note that in our case, the point \(t\) of the initial condition is \(t = 0\).

ic1 = dde.icbc.IC(geom, lambda X: -8, boundary, component=0)
ic2 = dde.icbc.IC(geom, lambda X: 7, boundary, component=1)
ic3 = dde.icbc.IC(geom, lambda X: 27, boundary, component=2)

Now me must assign the data from Lorenz.npz to the corresponding \(t\), \(x\), \(y\), and \(z\) values for training. First we retrieve the data to train the model. The data is split into "t" and "y", which correspond to time datapoints and the cartesian coodrinate datapoints (x, y, and z), respectivly.

def gen_traindata():
    data = np.load("dataset/Lorenz.npz")
    return data["t"], data["y"]

Then we organize and assign the train data.

observe_t, ob_y = gen_traindata()
observe_y0 = dde.icbc.PointSetBC(observe_t, ob_y[:, 0:1], component=0)
observe_y1 = dde.icbc.PointSetBC(observe_t, ob_y[:, 1:2], component=1)
observe_y2 = dde.icbc.PointSetBC(observe_t, ob_y[:, 2:3], component=2)

Now that the problem is fully setup, we define the PDE as:

data = dde.data.PDE(
    geom,
    Lorenz_system,
    [ic1, ic2, ic3, observe_y0, observe_y1, observe_y2],
    num_domain=400,
    num_boundary=2,
    anchors=observe_t,
)

Where num_domain is the number of points inside the domain, and num_boundary is the number of points on the boundary. anchors are extra points beyond num_domain and num_boundary used for training.

Next, we choose the network. Here, we use a fully connected neural network of depth 4 (i.e., 3 hidden layers) and width 40:

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

Now that the PDE problem and network have been created, we build a Model and choose the optimizer, learning rate, and provide the trainable variables C1, C2, and C3:

model = dde.Model(data, net)
model.compile("adam", lr=0.001, external_trainable_variables=[C1, C2, C3])
  variable = dde.callbacks.VariableValue(
  [C1, C2, C3], period=600, filename="variables.dat"
)

We then train the model for 60000 iterations:

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

Complete code

"""Backend supported: tensorflow.compat.v1, tensorflow, pytorch, jax"""
import deepxde as dde
import numpy as np


def gen_traindata():
    data = np.load("../dataset/Lorenz.npz")
    return data["t"], data["y"]


C1 = dde.Variable(1.0)
C2 = dde.Variable(1.0)
C3 = dde.Variable(1.0)

# Most backends
def Lorenz_system(x, y):
    """Lorenz system.
    dy1/dx = 10 * (y2 - y1)
    dy2/dx = y1 * (15 - y3) - y2
    dy3/dx = y1 * y2 - 8/3 * y3
    """
    y1, y2, y3 = y[:, 0:1], y[:, 1:2], y[:, 2:]
    dy1_x = dde.grad.jacobian(y, x, i=0)
    dy2_x = dde.grad.jacobian(y, x, i=1)
    dy3_x = dde.grad.jacobian(y, x, i=2)
    return [
        dy1_x - C1 * (y2 - y1),
        dy2_x - y1 * (C2 - y3) + y2,
        dy3_x - y1 * y2 + C3 * y3,
    ]


# Backend JAX
# def Lorenz_system(x, y, unknowns=[C1, C2, C3]):
#     C1, C2, C3 = unknowns
#     y_val, y_fn = y
#     y1, y2, y3 = y_val[:, 0:1], y_val[:, 1:2], y_val[:, 2:3]
#     dy1_x, _ = dde.grad.jacobian(y, x, i=0)
#     dy2_x, _ = dde.grad.jacobian(y, x, i=1)
#     dy3_x, _ = dde.grad.jacobian(y, x, i=2)
#     return [
#         dy1_x - C1 * (y2 - y1),
#         dy2_x - y1 * (C2 - y3) + y2,
#         dy3_x - y1 * y2 + C3 * y3,
#     ]


def boundary(_, on_initial):
    return on_initial


geom = dde.geometry.TimeDomain(0, 3)

# Initial conditions
ic1 = dde.icbc.IC(geom, lambda X: -8, boundary, component=0)
ic2 = dde.icbc.IC(geom, lambda X: 7, boundary, component=1)
ic3 = dde.icbc.IC(geom, lambda X: 27, boundary, component=2)

# Get the train data
observe_t, ob_y = gen_traindata()
observe_y0 = dde.icbc.PointSetBC(observe_t, ob_y[:, 0:1], component=0)
observe_y1 = dde.icbc.PointSetBC(observe_t, ob_y[:, 1:2], component=1)
observe_y2 = dde.icbc.PointSetBC(observe_t, ob_y[:, 2:3], component=2)

data = dde.data.PDE(
    geom,
    Lorenz_system,
    [ic1, ic2, ic3, observe_y0, observe_y1, observe_y2],
    num_domain=400,
    num_boundary=2,
    anchors=observe_t,
)

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

external_trainable_variables = [C1, C2, C3]
variable = dde.callbacks.VariableValue(
    external_trainable_variables, period=600, filename="variables.dat"
)

# train adam
model.compile(
    "adam", lr=0.001, external_trainable_variables=external_trainable_variables
)
losshistory, train_state = model.train(iterations=20000, callbacks=[variable])

# train lbfgs
model.compile("L-BFGS", external_trainable_variables=external_trainable_variables)
losshistory, train_state = model.train(callbacks=[variable])

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