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_23beaa91b261420b95b3d9ba80803f89 HEA cluster_cdf178ecf2a948bcadccd921e1ea61d5 Tower Chebyshev FM 6a7250504cc34a09a96090687f734233 0 1d041a3c0b1e45668bccfa1cbfba5e82 RX(1.0*acos(x)) 6a7250504cc34a09a96090687f734233--1d041a3c0b1e45668bccfa1cbfba5e82 9f10ff759da94cd1a26e3e791055f718 1 d75606ff28ef4a1b8f9dd515525e24a2 RX(theta₀) 1d041a3c0b1e45668bccfa1cbfba5e82--d75606ff28ef4a1b8f9dd515525e24a2 e3ee2d8c135f4e3ea2aa8c816657fb4e RY(theta₃) d75606ff28ef4a1b8f9dd515525e24a2--e3ee2d8c135f4e3ea2aa8c816657fb4e b3070b880aa44a9aa391bf17dece7a06 RX(theta₆) e3ee2d8c135f4e3ea2aa8c816657fb4e--b3070b880aa44a9aa391bf17dece7a06 157b7963e11f47e7903a6e913ae6dbc9 b3070b880aa44a9aa391bf17dece7a06--157b7963e11f47e7903a6e913ae6dbc9 cf4340ac8e424748bf80fac83a64798e 157b7963e11f47e7903a6e913ae6dbc9--cf4340ac8e424748bf80fac83a64798e b4ff246fd8b54ba081890188642793c0 RX(theta₉) cf4340ac8e424748bf80fac83a64798e--b4ff246fd8b54ba081890188642793c0 277124d0236747a39eccb125a3d7f067 RY(theta₁₂) b4ff246fd8b54ba081890188642793c0--277124d0236747a39eccb125a3d7f067 38a182c06b2a41528611ef769a8ee7d5 RX(theta₁₅) 277124d0236747a39eccb125a3d7f067--38a182c06b2a41528611ef769a8ee7d5 f25cebd9ae5b42af9fac8a5dd0d8e045 38a182c06b2a41528611ef769a8ee7d5--f25cebd9ae5b42af9fac8a5dd0d8e045 2fc83921108d4481aac9afc14f509a2c f25cebd9ae5b42af9fac8a5dd0d8e045--2fc83921108d4481aac9afc14f509a2c 5ea043beb7c94b0b9d04836f2f6d2b37 RX(theta₁₈) 2fc83921108d4481aac9afc14f509a2c--5ea043beb7c94b0b9d04836f2f6d2b37 ce71e3b39cac4868b7eb404b99568d09 RY(theta₂₁) 5ea043beb7c94b0b9d04836f2f6d2b37--ce71e3b39cac4868b7eb404b99568d09 8fe25a94836e45f6ae9c2deb66a65f2b RX(theta₂₄) ce71e3b39cac4868b7eb404b99568d09--8fe25a94836e45f6ae9c2deb66a65f2b 3827e829643e4c509a2116b8e8d4cc48 8fe25a94836e45f6ae9c2deb66a65f2b--3827e829643e4c509a2116b8e8d4cc48 a81ddeb050ab4706b5594a5a2458f8af 3827e829643e4c509a2116b8e8d4cc48--a81ddeb050ab4706b5594a5a2458f8af 56961f8351a84d06b1a34fd1ca143f4e a81ddeb050ab4706b5594a5a2458f8af--56961f8351a84d06b1a34fd1ca143f4e f47c52f9717c4fb28dff06a62d98abee 363eaece54954d5aba662d933d742ee6 RX(2.0*acos(x)) 9f10ff759da94cd1a26e3e791055f718--363eaece54954d5aba662d933d742ee6 89af778c32094cd9b85576d3d43c0548 2 f25f388ac8ad4358905a28f1c600bddf RX(theta₁) 363eaece54954d5aba662d933d742ee6--f25f388ac8ad4358905a28f1c600bddf 9dd56fea97d942898a061c35eff5f9ec RY(theta₄) f25f388ac8ad4358905a28f1c600bddf--9dd56fea97d942898a061c35eff5f9ec 428975e2ccd04816bfa0e37aef50573e RX(theta₇) 9dd56fea97d942898a061c35eff5f9ec--428975e2ccd04816bfa0e37aef50573e 88dc04b56b57477386e81663ac88eaff X 428975e2ccd04816bfa0e37aef50573e--88dc04b56b57477386e81663ac88eaff 88dc04b56b57477386e81663ac88eaff--157b7963e11f47e7903a6e913ae6dbc9 f52e0194b2ec41359003061162ae3f7d 88dc04b56b57477386e81663ac88eaff--f52e0194b2ec41359003061162ae3f7d fe4337e89edb49fb95437d40ff2db58b RX(theta₁₀) f52e0194b2ec41359003061162ae3f7d--fe4337e89edb49fb95437d40ff2db58b e803f3fdca2243f1808f85701da1dfd3 RY(theta₁₃) fe4337e89edb49fb95437d40ff2db58b--e803f3fdca2243f1808f85701da1dfd3 b9f5c33690644e079fe3065e27a89cd5 RX(theta₁₆) e803f3fdca2243f1808f85701da1dfd3--b9f5c33690644e079fe3065e27a89cd5 4e8221ee701148ba9eef704644f185ec X b9f5c33690644e079fe3065e27a89cd5--4e8221ee701148ba9eef704644f185ec 4e8221ee701148ba9eef704644f185ec--f25cebd9ae5b42af9fac8a5dd0d8e045 e9b325d9e8e046adbabaf6207b71b497 4e8221ee701148ba9eef704644f185ec--e9b325d9e8e046adbabaf6207b71b497 91c721c3e4f548efb5bd1efc603a6bea RX(theta₁₉) e9b325d9e8e046adbabaf6207b71b497--91c721c3e4f548efb5bd1efc603a6bea fd8c067a3160466d87dfbca7a605983b RY(theta₂₂) 91c721c3e4f548efb5bd1efc603a6bea--fd8c067a3160466d87dfbca7a605983b 81d01ba8428a48f584704149ccecb27c RX(theta₂₅) fd8c067a3160466d87dfbca7a605983b--81d01ba8428a48f584704149ccecb27c c4d90caf7a1f473a95c1c0eec904e75f X 81d01ba8428a48f584704149ccecb27c--c4d90caf7a1f473a95c1c0eec904e75f c4d90caf7a1f473a95c1c0eec904e75f--3827e829643e4c509a2116b8e8d4cc48 7d0611effde6464693e0ad7decf165a5 c4d90caf7a1f473a95c1c0eec904e75f--7d0611effde6464693e0ad7decf165a5 7d0611effde6464693e0ad7decf165a5--f47c52f9717c4fb28dff06a62d98abee 5b1b0f11393742aab4f803dacc4bfba9 e44ed88ecb9b49d08eb257e68422857a RX(3.0*acos(x)) 89af778c32094cd9b85576d3d43c0548--e44ed88ecb9b49d08eb257e68422857a fd7e209214734a549ff50e50a4e5986e RX(theta₂) e44ed88ecb9b49d08eb257e68422857a--fd7e209214734a549ff50e50a4e5986e 8b010bbea86d41928d7eede8dfb2daa5 RY(theta₅) fd7e209214734a549ff50e50a4e5986e--8b010bbea86d41928d7eede8dfb2daa5 c03e6d98aab24f689d2759cd8f62d8ea RX(theta₈) 8b010bbea86d41928d7eede8dfb2daa5--c03e6d98aab24f689d2759cd8f62d8ea 0bcbdca4add24a9488e654017650f0be c03e6d98aab24f689d2759cd8f62d8ea--0bcbdca4add24a9488e654017650f0be cfef928e80ca4b97954480f065033da6 X 0bcbdca4add24a9488e654017650f0be--cfef928e80ca4b97954480f065033da6 cfef928e80ca4b97954480f065033da6--f52e0194b2ec41359003061162ae3f7d 19f0a6e3a60e430186d6425efdc77870 RX(theta₁₁) cfef928e80ca4b97954480f065033da6--19f0a6e3a60e430186d6425efdc77870 eca9825f31a847859c8da4322b9eec1a RY(theta₁₄) 19f0a6e3a60e430186d6425efdc77870--eca9825f31a847859c8da4322b9eec1a 427b6fa1ae324fac9d09c4bb37aaa221 RX(theta₁₇) eca9825f31a847859c8da4322b9eec1a--427b6fa1ae324fac9d09c4bb37aaa221 17b04d9c7aad40bfa632cd741845d90c 427b6fa1ae324fac9d09c4bb37aaa221--17b04d9c7aad40bfa632cd741845d90c 4e36a0ab01074a5981628f9c5780aef6 X 17b04d9c7aad40bfa632cd741845d90c--4e36a0ab01074a5981628f9c5780aef6 4e36a0ab01074a5981628f9c5780aef6--e9b325d9e8e046adbabaf6207b71b497 5ce979273d6e42ffb060051900225d6c RX(theta₂₀) 4e36a0ab01074a5981628f9c5780aef6--5ce979273d6e42ffb060051900225d6c cb79d2351c5740adb872e5696c031903 RY(theta₂₃) 5ce979273d6e42ffb060051900225d6c--cb79d2351c5740adb872e5696c031903 374e2ee578644f7fad02863f32f357ad RX(theta₂₆) cb79d2351c5740adb872e5696c031903--374e2ee578644f7fad02863f32f357ad f08f9d9e01254e199451aa8c12e1eb3f 374e2ee578644f7fad02863f32f357ad--f08f9d9e01254e199451aa8c12e1eb3f eaddf28fae7041cdac2b214d3475b3f2 X f08f9d9e01254e199451aa8c12e1eb3f--eaddf28fae7041cdac2b214d3475b3f2 eaddf28fae7041cdac2b214d3475b3f2--7d0611effde6464693e0ad7decf165a5 eaddf28fae7041cdac2b214d3475b3f2--5b1b0f11393742aab4f803dacc4bfba9

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-26T12:34:03.940463 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-26T12:34:12.132288 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References