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_c6415200990e4232b671508841114fe4 HEA cluster_4b19c57a5d9749bbb7b25f032c71f46f Tower Chebyshev FM cad8dcdfcbef49c7a6b68971b7bd42cc 0 039005b058f54fdfa9e4b3c1572f89b8 RX(1.0*acos(x)) cad8dcdfcbef49c7a6b68971b7bd42cc--039005b058f54fdfa9e4b3c1572f89b8 9fa082ffe46b41aca5ab62af1162bb9f 1 26147024f7644bcb9da6611f42985fbe RX(theta₀) 039005b058f54fdfa9e4b3c1572f89b8--26147024f7644bcb9da6611f42985fbe 008d37c91bf04aa0874fdd4b1228aff9 RY(theta₃) 26147024f7644bcb9da6611f42985fbe--008d37c91bf04aa0874fdd4b1228aff9 eec155bd81874c2dad9fabcd4ac717ad RX(theta₆) 008d37c91bf04aa0874fdd4b1228aff9--eec155bd81874c2dad9fabcd4ac717ad 9ead2d80835c42318ac97116f68cb218 eec155bd81874c2dad9fabcd4ac717ad--9ead2d80835c42318ac97116f68cb218 1e54fd7e00e34b70aee5505779cddb87 9ead2d80835c42318ac97116f68cb218--1e54fd7e00e34b70aee5505779cddb87 36b44eea0f5743d184a768920a2ae95b RX(theta₉) 1e54fd7e00e34b70aee5505779cddb87--36b44eea0f5743d184a768920a2ae95b c863cb826c684cfba8fc99a85e4764f7 RY(theta₁₂) 36b44eea0f5743d184a768920a2ae95b--c863cb826c684cfba8fc99a85e4764f7 9c157c9a7dbb40c280e29b5e1d29d91a RX(theta₁₅) c863cb826c684cfba8fc99a85e4764f7--9c157c9a7dbb40c280e29b5e1d29d91a 6aa98df2640247169586f8a15b6a3b5a 9c157c9a7dbb40c280e29b5e1d29d91a--6aa98df2640247169586f8a15b6a3b5a 2b95790c35054beaad3600a904f64490 6aa98df2640247169586f8a15b6a3b5a--2b95790c35054beaad3600a904f64490 88e7fbe63e62499b9a7467ccb0b8e9f2 RX(theta₁₈) 2b95790c35054beaad3600a904f64490--88e7fbe63e62499b9a7467ccb0b8e9f2 fd241377338843de8d2f51756bf8cbe8 RY(theta₂₁) 88e7fbe63e62499b9a7467ccb0b8e9f2--fd241377338843de8d2f51756bf8cbe8 d7ed78ef97d74e1eb77ec860ce227224 RX(theta₂₄) fd241377338843de8d2f51756bf8cbe8--d7ed78ef97d74e1eb77ec860ce227224 04f1c4da9eb4451aab9ebb6d51fd654a d7ed78ef97d74e1eb77ec860ce227224--04f1c4da9eb4451aab9ebb6d51fd654a 2b60190a00df41db93672f060be2c015 04f1c4da9eb4451aab9ebb6d51fd654a--2b60190a00df41db93672f060be2c015 3c1752aeb21c41c48c8a1f604986828e 2b60190a00df41db93672f060be2c015--3c1752aeb21c41c48c8a1f604986828e 984a89c25838484ea3cbb6c03d12f843 fb71cc9f5d7245d086ea73963b9addbb RX(2.0*acos(x)) 9fa082ffe46b41aca5ab62af1162bb9f--fb71cc9f5d7245d086ea73963b9addbb 785aff27e1d84dd2b194240a950dbb77 2 89aee1e285c24e6ba32847eaf984eb48 RX(theta₁) fb71cc9f5d7245d086ea73963b9addbb--89aee1e285c24e6ba32847eaf984eb48 d8c3a8e208754af1bdc9c57571ac0549 RY(theta₄) 89aee1e285c24e6ba32847eaf984eb48--d8c3a8e208754af1bdc9c57571ac0549 92ee6b1cd6cc4296b6ab201c5cff955b RX(theta₇) d8c3a8e208754af1bdc9c57571ac0549--92ee6b1cd6cc4296b6ab201c5cff955b 6504faa992b141c1b995408454177860 X 92ee6b1cd6cc4296b6ab201c5cff955b--6504faa992b141c1b995408454177860 6504faa992b141c1b995408454177860--9ead2d80835c42318ac97116f68cb218 46d9f3905e224f9dbe06d0b0ef7a4b81 6504faa992b141c1b995408454177860--46d9f3905e224f9dbe06d0b0ef7a4b81 2fe186e158c142e693b47f933d3bd843 RX(theta₁₀) 46d9f3905e224f9dbe06d0b0ef7a4b81--2fe186e158c142e693b47f933d3bd843 adb829922ed14c789b5424a0fe2ef6b0 RY(theta₁₃) 2fe186e158c142e693b47f933d3bd843--adb829922ed14c789b5424a0fe2ef6b0 7d9dfc9662844c9cbadb415713ef16a8 RX(theta₁₆) adb829922ed14c789b5424a0fe2ef6b0--7d9dfc9662844c9cbadb415713ef16a8 d4d5e93b18b841ba9c394f32387571ab X 7d9dfc9662844c9cbadb415713ef16a8--d4d5e93b18b841ba9c394f32387571ab d4d5e93b18b841ba9c394f32387571ab--6aa98df2640247169586f8a15b6a3b5a bb9e0eb90f754362a674d3336587326f d4d5e93b18b841ba9c394f32387571ab--bb9e0eb90f754362a674d3336587326f 26c28e305cfd49a5aecccffeb9efbd58 RX(theta₁₉) bb9e0eb90f754362a674d3336587326f--26c28e305cfd49a5aecccffeb9efbd58 fdd195af8387489285b13c59fb660911 RY(theta₂₂) 26c28e305cfd49a5aecccffeb9efbd58--fdd195af8387489285b13c59fb660911 4a7b2a08317346318af17abbdfecc6c0 RX(theta₂₅) fdd195af8387489285b13c59fb660911--4a7b2a08317346318af17abbdfecc6c0 8035dd415d5f424bbe3c6b388c2143cb X 4a7b2a08317346318af17abbdfecc6c0--8035dd415d5f424bbe3c6b388c2143cb 8035dd415d5f424bbe3c6b388c2143cb--04f1c4da9eb4451aab9ebb6d51fd654a 08ea928b835948029558f804f55a500c 8035dd415d5f424bbe3c6b388c2143cb--08ea928b835948029558f804f55a500c 08ea928b835948029558f804f55a500c--984a89c25838484ea3cbb6c03d12f843 28362ee60eb64ed4a3ab953ebd9b9e7b e25418b05db84500bfe2e46cff84d94a RX(3.0*acos(x)) 785aff27e1d84dd2b194240a950dbb77--e25418b05db84500bfe2e46cff84d94a 1e568dbad0844f25bdacc472e42809a0 RX(theta₂) e25418b05db84500bfe2e46cff84d94a--1e568dbad0844f25bdacc472e42809a0 525b85f8d8ea4508b78b9ec9cf829bc3 RY(theta₅) 1e568dbad0844f25bdacc472e42809a0--525b85f8d8ea4508b78b9ec9cf829bc3 5d106b0d18014d3a94955ccbb744b89e RX(theta₈) 525b85f8d8ea4508b78b9ec9cf829bc3--5d106b0d18014d3a94955ccbb744b89e cd1018ae3ae54435a9abfba1e23f4909 5d106b0d18014d3a94955ccbb744b89e--cd1018ae3ae54435a9abfba1e23f4909 718825e368cb4492addbd1f15b429964 X cd1018ae3ae54435a9abfba1e23f4909--718825e368cb4492addbd1f15b429964 718825e368cb4492addbd1f15b429964--46d9f3905e224f9dbe06d0b0ef7a4b81 c11bd3d4be9f493d818bf76335896f1f RX(theta₁₁) 718825e368cb4492addbd1f15b429964--c11bd3d4be9f493d818bf76335896f1f 93b5f9ceedea4034bbbb2eaed08684e0 RY(theta₁₄) c11bd3d4be9f493d818bf76335896f1f--93b5f9ceedea4034bbbb2eaed08684e0 174531497a874e3bb305f5d3dba2404a RX(theta₁₇) 93b5f9ceedea4034bbbb2eaed08684e0--174531497a874e3bb305f5d3dba2404a b5d84d3750fd4d5abfb0dc8f41505017 174531497a874e3bb305f5d3dba2404a--b5d84d3750fd4d5abfb0dc8f41505017 cc6805c0463744d394b2ba74ec731927 X b5d84d3750fd4d5abfb0dc8f41505017--cc6805c0463744d394b2ba74ec731927 cc6805c0463744d394b2ba74ec731927--bb9e0eb90f754362a674d3336587326f 5d07f5dbd1ac45b8ae6d3f28ab8122e1 RX(theta₂₀) cc6805c0463744d394b2ba74ec731927--5d07f5dbd1ac45b8ae6d3f28ab8122e1 1b4b2f1165184ddcbeae6cd62fd9fa62 RY(theta₂₃) 5d07f5dbd1ac45b8ae6d3f28ab8122e1--1b4b2f1165184ddcbeae6cd62fd9fa62 0ac236d7f01d4710bc08f5e90792ef75 RX(theta₂₆) 1b4b2f1165184ddcbeae6cd62fd9fa62--0ac236d7f01d4710bc08f5e90792ef75 b54fc1d357ce456495c867049df05d8e 0ac236d7f01d4710bc08f5e90792ef75--b54fc1d357ce456495c867049df05d8e b6acc4f2dadb442296982d47ebd24838 X b54fc1d357ce456495c867049df05d8e--b6acc4f2dadb442296982d47ebd24838 b6acc4f2dadb442296982d47ebd24838--08ea928b835948029558f804f55a500c b6acc4f2dadb442296982d47ebd24838--28362ee60eb64ed4a3ab953ebd9b9e7b

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-05-03T08:23:40.846644 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-05-03T08:23:48.219305 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References