Skip to content

Solving a 1D ODE

In this tutorial we will show how to use Qadence to solve a basic 1D Ordinary Differential Equation (ODE) with a QNN using Differentiable Quantum Circuits (DQC) 1.

Consider the following non-linear ODE and boundary condition:

\[ \frac{df}{dx}= 5\times(4x^3+x^2-2x-\frac12), \qquad f(0)=0 \]

It admits an exact solution:

\[ f(x)=5\times(x^4+\frac13x^3-x^2-\frac12x) \]

Our goal will be to find this solution for \(x\in[-1, 1]\).

import torch

def dfdx_equation(x: torch.Tensor) -> torch.Tensor:
    """Derivative as per the equation."""
    return 5*(4*x**3 + x**2 - 2*x - 0.5)

For the purpose of this tutorial, we will compute the derivative of the circuit using torch.autograd. The point of the DQC algorithm is to use differentiable circuits with parameter shift rules. In Qadence, PSR is implemented directly as custom overrides of the derivative function in the autograd engine, and thus we can later change the derivative method for the model itself if we wish.

def calc_deriv(outputs: torch.Tensor, inputs: torch.Tensor) -> torch.Tensor:
    """Compute a derivative of model that learns f(x), computes df/dx using torch.autograd."""
    grad = torch.autograd.grad(
        outputs=outputs,
        inputs=inputs,
        grad_outputs = torch.ones_like(inputs),
        create_graph = True,
        retain_graph = True,
    )[0]
    return grad

Defining the loss function

The essential part of solving this problem is to define the right loss function to represent our goal. In this case, we want to define a model that has the capacity to learn the target solution, and we want to minimize: - The derivative of this model in comparison with the exact derivative in the equation; - The output of the model at the boundary in comparison with the value for the boundary condition;

We can write it like so:

# Mean-squared error as the comparison criterion
criterion = torch.nn.MSELoss()

def loss_fn(model: torch.nn.Module, inputs: torch.Tensor) -> torch.Tensor:
    """Loss function encoding the problem to solve."""
    # Equation loss
    model_output = model(inputs)
    deriv_model = calc_deriv(model_output, inputs)
    deriv_exact = dfdx_equation(inputs)
    ode_loss = criterion(deriv_model, deriv_exact)

    # Boundary loss, f(0) = 0
    boundary_model = model(torch.tensor([[0.0]]))
    boundary_exact = torch.tensor([[0.0]])
    boundary_loss = criterion(boundary_model, boundary_exact)

    return ode_loss + boundary_loss

Different loss criterions could be considered, and we could also play with the balance between the sum of the two loss terms. For now, let's proceed with the definition above.

Note that so far we have not used any quantum specific assumption, and we could in principle use the same loss function with a classical neural network.

Defining a QNN with Qadence

Now, we can finally use Qadence to write a QNN. We will use a feature map to encode the input values, a trainable ansatz circuit, and an observable to measure as the output.

from qadence import feature_map, hea, chain
from qadence import QNN, QuantumCircuit, Z
from qadence.types import BasisSet, ReuploadScaling

n_qubits = 3
depth = 3

# Feature map
fm = feature_map(
    n_qubits = n_qubits,
    param = "x",
    fm_type = BasisSet.CHEBYSHEV,
    reupload_scaling = ReuploadScaling.TOWER,
)

# Ansatz
ansatz = hea(n_qubits = n_qubits, depth = depth)

# Observable
observable = Z(0)

circuit = QuantumCircuit(n_qubits, chain(fm, ansatz))
model = QNN(circuit = circuit, observable = observable, inputs = ["x"])

We used a Chebyshev feature map with a tower-like scaling of the input reupload, and a standard hardware-efficient ansatz. You can check the qml constructors tutorial to see how you can customize these components. In the observable, for now we consider the simple case of measuring the magnetization of the first qubit.

from qadence.draw import display

# display(circuit)
%3 cluster_56d09ff1cec5462eb298c65184b897cd HEA cluster_4e3e7ae79d4045508d5cb43714a34ea4 Tower Chebyshev FM 403ef099fac241139572236deded26ff 0 d3bafade406342c6acb85d9e9b934016 RX(1.0*acos(x)) 403ef099fac241139572236deded26ff--d3bafade406342c6acb85d9e9b934016 99f4b0fea4204fc79eb1c36329aedefe 1 1736bf2f0405451ebadf07f908e2aab9 RX(theta₀) d3bafade406342c6acb85d9e9b934016--1736bf2f0405451ebadf07f908e2aab9 7c0f21f1833f4d1bb14500cadbc08c52 RY(theta₃) 1736bf2f0405451ebadf07f908e2aab9--7c0f21f1833f4d1bb14500cadbc08c52 7b6136b162694f70a1d682b7559a7a44 RX(theta₆) 7c0f21f1833f4d1bb14500cadbc08c52--7b6136b162694f70a1d682b7559a7a44 3cd931da8787450f8972de46f5aeea88 7b6136b162694f70a1d682b7559a7a44--3cd931da8787450f8972de46f5aeea88 3bbe572e96194451b789233febcbea95 3cd931da8787450f8972de46f5aeea88--3bbe572e96194451b789233febcbea95 f101862fd2a54097956ab4a7141f40a8 RX(theta₉) 3bbe572e96194451b789233febcbea95--f101862fd2a54097956ab4a7141f40a8 a7a6f0fc0aed4795937ff9ed40c7489b RY(theta₁₂) f101862fd2a54097956ab4a7141f40a8--a7a6f0fc0aed4795937ff9ed40c7489b b412cbb8bf1e47c0a9735ac81fa0d192 RX(theta₁₅) a7a6f0fc0aed4795937ff9ed40c7489b--b412cbb8bf1e47c0a9735ac81fa0d192 5a367b56d2fc43eebac41388673b1a08 b412cbb8bf1e47c0a9735ac81fa0d192--5a367b56d2fc43eebac41388673b1a08 7f160ea5ab814c0b9fe8ae6b33c35255 5a367b56d2fc43eebac41388673b1a08--7f160ea5ab814c0b9fe8ae6b33c35255 5d15eb48a4754f1bb6840aa6bd93493f RX(theta₁₈) 7f160ea5ab814c0b9fe8ae6b33c35255--5d15eb48a4754f1bb6840aa6bd93493f de7881b56e65417895fc078ecf63dc79 RY(theta₂₁) 5d15eb48a4754f1bb6840aa6bd93493f--de7881b56e65417895fc078ecf63dc79 a2e6c896f86d43c998d70abccd836abf RX(theta₂₄) de7881b56e65417895fc078ecf63dc79--a2e6c896f86d43c998d70abccd836abf 57a578ce3ac14527a65c44da399e357a a2e6c896f86d43c998d70abccd836abf--57a578ce3ac14527a65c44da399e357a ff2dd66f85a143fca586734e34035990 57a578ce3ac14527a65c44da399e357a--ff2dd66f85a143fca586734e34035990 4f13385ab3ce45d8ab478188577ac12a ff2dd66f85a143fca586734e34035990--4f13385ab3ce45d8ab478188577ac12a def0b7a8ba82482b857eb438db546e27 3a699ef5d5014ff2aef29244cf9a8039 RX(2.0*acos(x)) 99f4b0fea4204fc79eb1c36329aedefe--3a699ef5d5014ff2aef29244cf9a8039 3d4a7f07d3ac4ebf90900c139f61427c 2 12fc6d3c845f4c7fba8fe99ca845bb91 RX(theta₁) 3a699ef5d5014ff2aef29244cf9a8039--12fc6d3c845f4c7fba8fe99ca845bb91 f3c402501a204410a0493c3b6f4e9670 RY(theta₄) 12fc6d3c845f4c7fba8fe99ca845bb91--f3c402501a204410a0493c3b6f4e9670 b7c619aed2a441bda4500c2958b5ea16 RX(theta₇) f3c402501a204410a0493c3b6f4e9670--b7c619aed2a441bda4500c2958b5ea16 3754ccc575cd4d32990e6e89006e716d X b7c619aed2a441bda4500c2958b5ea16--3754ccc575cd4d32990e6e89006e716d 3754ccc575cd4d32990e6e89006e716d--3cd931da8787450f8972de46f5aeea88 7b55865959f54d20bf53b572d4961d21 3754ccc575cd4d32990e6e89006e716d--7b55865959f54d20bf53b572d4961d21 538b9c4c049d400795b02e49a8b47f0d RX(theta₁₀) 7b55865959f54d20bf53b572d4961d21--538b9c4c049d400795b02e49a8b47f0d f3afee0f31b44ca78ee1a3f49beaabd4 RY(theta₁₃) 538b9c4c049d400795b02e49a8b47f0d--f3afee0f31b44ca78ee1a3f49beaabd4 e3e2dbbd9b2440c687246d929a994095 RX(theta₁₆) f3afee0f31b44ca78ee1a3f49beaabd4--e3e2dbbd9b2440c687246d929a994095 f609259880e34c86a8505072677dbfa0 X e3e2dbbd9b2440c687246d929a994095--f609259880e34c86a8505072677dbfa0 f609259880e34c86a8505072677dbfa0--5a367b56d2fc43eebac41388673b1a08 1e7bb1f0470a47989c5f60030389f93a f609259880e34c86a8505072677dbfa0--1e7bb1f0470a47989c5f60030389f93a e2f3412f1fe644bcbfe1254e794994d9 RX(theta₁₉) 1e7bb1f0470a47989c5f60030389f93a--e2f3412f1fe644bcbfe1254e794994d9 890d0bb58282471eb07a87028bf84347 RY(theta₂₂) e2f3412f1fe644bcbfe1254e794994d9--890d0bb58282471eb07a87028bf84347 0498ddb6d3c444d3b3a740b5b2600e02 RX(theta₂₅) 890d0bb58282471eb07a87028bf84347--0498ddb6d3c444d3b3a740b5b2600e02 e8e1704545734d1db1d374c156d958c5 X 0498ddb6d3c444d3b3a740b5b2600e02--e8e1704545734d1db1d374c156d958c5 e8e1704545734d1db1d374c156d958c5--57a578ce3ac14527a65c44da399e357a fd440de9ede94243841c395754c5a4cc e8e1704545734d1db1d374c156d958c5--fd440de9ede94243841c395754c5a4cc fd440de9ede94243841c395754c5a4cc--def0b7a8ba82482b857eb438db546e27 f00a150371084ffc814c6e65e2397b58 0d6e046bafd8443aaf8490f4e1fdb03f RX(3.0*acos(x)) 3d4a7f07d3ac4ebf90900c139f61427c--0d6e046bafd8443aaf8490f4e1fdb03f aa3aa289769c49e3b9a72f9ca47aae3f RX(theta₂) 0d6e046bafd8443aaf8490f4e1fdb03f--aa3aa289769c49e3b9a72f9ca47aae3f d8df402b7ca24d1f875992249c3f1ab5 RY(theta₅) aa3aa289769c49e3b9a72f9ca47aae3f--d8df402b7ca24d1f875992249c3f1ab5 877047231761480db5c527d3ae6d73ee RX(theta₈) d8df402b7ca24d1f875992249c3f1ab5--877047231761480db5c527d3ae6d73ee 1fb34c4027fb46ac8b1adef2da0d1484 877047231761480db5c527d3ae6d73ee--1fb34c4027fb46ac8b1adef2da0d1484 40cbe1c67e03444cafc202af552f4783 X 1fb34c4027fb46ac8b1adef2da0d1484--40cbe1c67e03444cafc202af552f4783 40cbe1c67e03444cafc202af552f4783--7b55865959f54d20bf53b572d4961d21 03e36538c8ad45d580ab1b9f87ef0b24 RX(theta₁₁) 40cbe1c67e03444cafc202af552f4783--03e36538c8ad45d580ab1b9f87ef0b24 fd4c64f9b60342b98622d1897f5337a2 RY(theta₁₄) 03e36538c8ad45d580ab1b9f87ef0b24--fd4c64f9b60342b98622d1897f5337a2 f2eebb3d07bb4357b1d9c6b8ff001843 RX(theta₁₇) fd4c64f9b60342b98622d1897f5337a2--f2eebb3d07bb4357b1d9c6b8ff001843 2a43d2e994c4438d871d88f2c0b47b74 f2eebb3d07bb4357b1d9c6b8ff001843--2a43d2e994c4438d871d88f2c0b47b74 6416bf24ebaa4f9284c3cfee65b1e691 X 2a43d2e994c4438d871d88f2c0b47b74--6416bf24ebaa4f9284c3cfee65b1e691 6416bf24ebaa4f9284c3cfee65b1e691--1e7bb1f0470a47989c5f60030389f93a 117d39ff20cc4968928a3f793b730122 RX(theta₂₀) 6416bf24ebaa4f9284c3cfee65b1e691--117d39ff20cc4968928a3f793b730122 db33315e10d14a2aa9904be721174dbc RY(theta₂₃) 117d39ff20cc4968928a3f793b730122--db33315e10d14a2aa9904be721174dbc f9495b9a6a1c4081b8529b2139a60caf RX(theta₂₆) db33315e10d14a2aa9904be721174dbc--f9495b9a6a1c4081b8529b2139a60caf 4b895e7735a4439390a6991a95567d71 f9495b9a6a1c4081b8529b2139a60caf--4b895e7735a4439390a6991a95567d71 0531d76329a347399749752c4d515179 X 4b895e7735a4439390a6991a95567d71--0531d76329a347399749752c4d515179 0531d76329a347399749752c4d515179--fd440de9ede94243841c395754c5a4cc 0531d76329a347399749752c4d515179--f00a150371084ffc814c6e65e2397b58

Training the model

Now that the model is defined we can proceed with the training. the QNN class can be used like any other torch.nn.Module. Here we write a simple training loop, but you can also look at the ml tools tutorial to use the convenience training functions that Qadence provides.

To train the model, we will select a random set of collocation points uniformly distributed within \(-1.0< x <1.0\) and compute the loss function for those points.

n_epochs = 200
n_points = 10

xmin = -0.99
xmax = 0.99

optimizer = torch.optim.Adam(model.parameters(), lr = 0.1)

for epoch in range(n_epochs):
    optimizer.zero_grad()

    # Training data. We unsqueeze essentially making each batch have a single x value.
    x_train = (xmin + (xmax-xmin)*torch.rand(n_points, requires_grad = True)).unsqueeze(1)

    loss = loss_fn(inputs = x_train, model = model)
    loss.backward()
    optimizer.step()

Note the values of \(x\) are only picked from \(x\in[-0.99, 0.99]\) since we are using a Chebyshev feature map, and derivative of \(\text{acos}(x)\) diverges at \(-1\) and \(1\).

Plotting the results

import matplotlib.pyplot as plt

def f_exact(x: torch.Tensor) -> torch.Tensor:
    return 5*(x**4 + (1/3)*x**3 - x**2 - 0.5*x)

x_test = torch.arange(xmin, xmax, step = 0.01).unsqueeze(1)

result_exact = f_exact(x_test).flatten()

result_model = model(x_test).flatten().detach()

plt.plot(x_test, result_exact, label = "Exact solution")
plt.plot(x_test, result_model, label = " Trained model")
2024-07-12T10:07:28.058140 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

Clearly, the result is not optimal.

Improving the solution

One point to consider when defining the QNN is the possible output range, which is bounded by the spectrum of the chosen observable. For the magnetization of a single qubit, this means that the output is bounded between -1 and 1, which we can clearly see in the plot.

One option would be to define the observable as the total magnetization over all qubits, which would allow a range of -3 to 3.

from qadence import add

observable = add(Z(i) for i in range(n_qubits))

model = QNN(circuit = circuit, observable = observable, inputs = ["x"])

optimizer = torch.optim.Adam(model.parameters(), lr = 0.1)

for epoch in range(n_epochs):
    optimizer.zero_grad()

    # Training data
    x_train = (xmin + (xmax-xmin)*torch.rand(n_points, requires_grad = True)).unsqueeze(1)

    loss = loss_fn(inputs = x_train, model = model)
    loss.backward()
    optimizer.step()

And we again plot the result:

x_test = torch.arange(xmin, xmax, step = 0.01).unsqueeze(1)

result_exact = f_exact(x_test).flatten()

result_model = model(x_test).flatten().detach()

plt.plot(x_test, result_exact, label = "Exact solution")
plt.plot(x_test, result_model, label = "Trained model")
2024-07-12T10:07:35.951280 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References