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_636031c1fb4740909d669994768c86e5 HEA cluster_71f2c6d5dbe547268f16a963505e054c Tower Chebyshev FM 81577fdfc0d94065ac195a797e6609ef 0 e0e41bb5bcfd48ab830b7199729994ae RX(1.0*acos(x)) 81577fdfc0d94065ac195a797e6609ef--e0e41bb5bcfd48ab830b7199729994ae 86d019f1abfe4db9ad4f2fb2ca5129c6 1 9d47f535a650404fa7553e31ad9b19d1 RX(theta₀) e0e41bb5bcfd48ab830b7199729994ae--9d47f535a650404fa7553e31ad9b19d1 72eb2af1ae3e478f8dc8d7854f91d6b0 RY(theta₃) 9d47f535a650404fa7553e31ad9b19d1--72eb2af1ae3e478f8dc8d7854f91d6b0 069fa64e341a462691c423cde15894f5 RX(theta₆) 72eb2af1ae3e478f8dc8d7854f91d6b0--069fa64e341a462691c423cde15894f5 ce2c8e18080e4cd38a750c130c0b59c0 069fa64e341a462691c423cde15894f5--ce2c8e18080e4cd38a750c130c0b59c0 8cabe7e7c9884f93b1b01fc075b64fd9 ce2c8e18080e4cd38a750c130c0b59c0--8cabe7e7c9884f93b1b01fc075b64fd9 e819d8eb77ff45239fbdfce5aea4912c RX(theta₉) 8cabe7e7c9884f93b1b01fc075b64fd9--e819d8eb77ff45239fbdfce5aea4912c d4c1db7061604b84aef54830f5a8ebef RY(theta₁₂) e819d8eb77ff45239fbdfce5aea4912c--d4c1db7061604b84aef54830f5a8ebef 0e097cb6c19e47b191754da0fc34d8dc RX(theta₁₅) d4c1db7061604b84aef54830f5a8ebef--0e097cb6c19e47b191754da0fc34d8dc 09fa773fcd2849dd8baa290ab115a304 0e097cb6c19e47b191754da0fc34d8dc--09fa773fcd2849dd8baa290ab115a304 d50f8e8ef9884f44ba3f329e20feac27 09fa773fcd2849dd8baa290ab115a304--d50f8e8ef9884f44ba3f329e20feac27 934c6eb1ef7742fe9317481f03007aa8 RX(theta₁₈) d50f8e8ef9884f44ba3f329e20feac27--934c6eb1ef7742fe9317481f03007aa8 4d6b41065a9246f9b21663ca9d3f87a8 RY(theta₂₁) 934c6eb1ef7742fe9317481f03007aa8--4d6b41065a9246f9b21663ca9d3f87a8 463dccf703c840a1bd1b78bd9cc108b1 RX(theta₂₄) 4d6b41065a9246f9b21663ca9d3f87a8--463dccf703c840a1bd1b78bd9cc108b1 143d7cff631c408db87bb769f30bcbd3 463dccf703c840a1bd1b78bd9cc108b1--143d7cff631c408db87bb769f30bcbd3 83e75c737b5841109b21d60125f68233 143d7cff631c408db87bb769f30bcbd3--83e75c737b5841109b21d60125f68233 b5b1aeff473c46d39874994c9bb75a90 83e75c737b5841109b21d60125f68233--b5b1aeff473c46d39874994c9bb75a90 ef1b81c2cc8842e8a3e9cebfa2e4a334 1afa96a8035d46879e2eb5ef689967e4 RX(2.0*acos(x)) 86d019f1abfe4db9ad4f2fb2ca5129c6--1afa96a8035d46879e2eb5ef689967e4 819b7d5d58654b1c913b444571f5ebce 2 696cba7ef6cc47b797a9d4992db7def9 RX(theta₁) 1afa96a8035d46879e2eb5ef689967e4--696cba7ef6cc47b797a9d4992db7def9 68734870e0f24c778dc059c458fe7a11 RY(theta₄) 696cba7ef6cc47b797a9d4992db7def9--68734870e0f24c778dc059c458fe7a11 0ecd5b3ce2a749f9bb7ee7288c168051 RX(theta₇) 68734870e0f24c778dc059c458fe7a11--0ecd5b3ce2a749f9bb7ee7288c168051 9c83c3c245f945fba7946c03adf2c45e X 0ecd5b3ce2a749f9bb7ee7288c168051--9c83c3c245f945fba7946c03adf2c45e 9c83c3c245f945fba7946c03adf2c45e--ce2c8e18080e4cd38a750c130c0b59c0 2528e2710cbb4439acb4bcf3a1f41ecc 9c83c3c245f945fba7946c03adf2c45e--2528e2710cbb4439acb4bcf3a1f41ecc 9f6282f8c58f493993237d855a2b7eee RX(theta₁₀) 2528e2710cbb4439acb4bcf3a1f41ecc--9f6282f8c58f493993237d855a2b7eee fdf8f8f851a4411ab7529b5f4e77ecbd RY(theta₁₃) 9f6282f8c58f493993237d855a2b7eee--fdf8f8f851a4411ab7529b5f4e77ecbd 5169b7a213ea45aaa5d0aed89578cf26 RX(theta₁₆) fdf8f8f851a4411ab7529b5f4e77ecbd--5169b7a213ea45aaa5d0aed89578cf26 8c264ccbcd9e4882a449dfd7a3a650ec X 5169b7a213ea45aaa5d0aed89578cf26--8c264ccbcd9e4882a449dfd7a3a650ec 8c264ccbcd9e4882a449dfd7a3a650ec--09fa773fcd2849dd8baa290ab115a304 ca726b63a35440e7bd8fc3077f4a5345 8c264ccbcd9e4882a449dfd7a3a650ec--ca726b63a35440e7bd8fc3077f4a5345 0a68a50c06fd4661a6455d85cc2b7e44 RX(theta₁₉) ca726b63a35440e7bd8fc3077f4a5345--0a68a50c06fd4661a6455d85cc2b7e44 659ed15c38dc456f9ff3cdd0fa88fe67 RY(theta₂₂) 0a68a50c06fd4661a6455d85cc2b7e44--659ed15c38dc456f9ff3cdd0fa88fe67 9aeed5c309cd47e6b02c0aa40ecb2eea RX(theta₂₅) 659ed15c38dc456f9ff3cdd0fa88fe67--9aeed5c309cd47e6b02c0aa40ecb2eea 26066155ce6d459083d5ddb68ee88b67 X 9aeed5c309cd47e6b02c0aa40ecb2eea--26066155ce6d459083d5ddb68ee88b67 26066155ce6d459083d5ddb68ee88b67--143d7cff631c408db87bb769f30bcbd3 17cbcebce98640f494d74f922b3f61e5 26066155ce6d459083d5ddb68ee88b67--17cbcebce98640f494d74f922b3f61e5 17cbcebce98640f494d74f922b3f61e5--ef1b81c2cc8842e8a3e9cebfa2e4a334 9e42809099ac479b8ca05b6ac13a41c8 4bbf4fc5247f49f3b2b0981d5035feca RX(3.0*acos(x)) 819b7d5d58654b1c913b444571f5ebce--4bbf4fc5247f49f3b2b0981d5035feca b965369fd41543daad09b0d95f225cb8 RX(theta₂) 4bbf4fc5247f49f3b2b0981d5035feca--b965369fd41543daad09b0d95f225cb8 b3d1f4112f624aad9ea83265b7cfcf00 RY(theta₅) b965369fd41543daad09b0d95f225cb8--b3d1f4112f624aad9ea83265b7cfcf00 027882d7bf464037add42f86a8aee706 RX(theta₈) b3d1f4112f624aad9ea83265b7cfcf00--027882d7bf464037add42f86a8aee706 349a83c028d34b7d829546c9bce6d354 027882d7bf464037add42f86a8aee706--349a83c028d34b7d829546c9bce6d354 a1a6207f8e41479da85885dbd949ba68 X 349a83c028d34b7d829546c9bce6d354--a1a6207f8e41479da85885dbd949ba68 a1a6207f8e41479da85885dbd949ba68--2528e2710cbb4439acb4bcf3a1f41ecc be495be98c6f4d74adab00672a35d1eb RX(theta₁₁) a1a6207f8e41479da85885dbd949ba68--be495be98c6f4d74adab00672a35d1eb b35619c4b2454f589257649931a899bb RY(theta₁₄) be495be98c6f4d74adab00672a35d1eb--b35619c4b2454f589257649931a899bb ac5b8fea9c93448f8a886d30715765cc RX(theta₁₇) b35619c4b2454f589257649931a899bb--ac5b8fea9c93448f8a886d30715765cc 995b60a5acdc479a80673ffed9672ab2 ac5b8fea9c93448f8a886d30715765cc--995b60a5acdc479a80673ffed9672ab2 b12fade7784c48e78407d68ea9b521ed X 995b60a5acdc479a80673ffed9672ab2--b12fade7784c48e78407d68ea9b521ed b12fade7784c48e78407d68ea9b521ed--ca726b63a35440e7bd8fc3077f4a5345 8d1b6587050540e6a88a441dd60cccf6 RX(theta₂₀) b12fade7784c48e78407d68ea9b521ed--8d1b6587050540e6a88a441dd60cccf6 6ca435578dce44d3bfd3e349b982fdb7 RY(theta₂₃) 8d1b6587050540e6a88a441dd60cccf6--6ca435578dce44d3bfd3e349b982fdb7 0d60ab075d3548f5bff963077ce6931d RX(theta₂₆) 6ca435578dce44d3bfd3e349b982fdb7--0d60ab075d3548f5bff963077ce6931d 2542dc8faaba46699d25e26232539aca 0d60ab075d3548f5bff963077ce6931d--2542dc8faaba46699d25e26232539aca 622c38b6580c49fbb8a590298c4b7b0e X 2542dc8faaba46699d25e26232539aca--622c38b6580c49fbb8a590298c4b7b0e 622c38b6580c49fbb8a590298c4b7b0e--17cbcebce98640f494d74f922b3f61e5 622c38b6580c49fbb8a590298c4b7b0e--9e42809099ac479b8ca05b6ac13a41c8

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-11-27T09:05:29.701461 image/svg+xml Matplotlib v3.9.2, 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-11-27T09:05:37.787269 image/svg+xml Matplotlib v3.9.2, https://matplotlib.org/

References