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_02e5594b1b354a78b7b4d757ff5c77af HEA cluster_cb6951ff3d664547a56966711eec07e3 Tower Chebyshev FM 5a9bd60eb6414d56a1a40fa87860cb57 0 29ccb791a5994b459c32f6e19f2b07ca RX(1.0*acos(x)) 5a9bd60eb6414d56a1a40fa87860cb57--29ccb791a5994b459c32f6e19f2b07ca b29236be863948c6b2fbbb5cccf0009c 1 27088a6393804ae28b99942cf26d7a93 RX(theta₀) 29ccb791a5994b459c32f6e19f2b07ca--27088a6393804ae28b99942cf26d7a93 3434f567ef104f6b946b04eee2d3f13e RY(theta₃) 27088a6393804ae28b99942cf26d7a93--3434f567ef104f6b946b04eee2d3f13e d85fb49431c7451f8ee3f33f857ccf60 RX(theta₆) 3434f567ef104f6b946b04eee2d3f13e--d85fb49431c7451f8ee3f33f857ccf60 4803df12a07f4f39b5a8bcefbea105bb d85fb49431c7451f8ee3f33f857ccf60--4803df12a07f4f39b5a8bcefbea105bb e20b85cf15214cefa3b30af56b3613ff 4803df12a07f4f39b5a8bcefbea105bb--e20b85cf15214cefa3b30af56b3613ff 15edd316ce054ca183eaca6401517ae8 RX(theta₉) e20b85cf15214cefa3b30af56b3613ff--15edd316ce054ca183eaca6401517ae8 8f76f794b6a84bca970221e1b45af2b8 RY(theta₁₂) 15edd316ce054ca183eaca6401517ae8--8f76f794b6a84bca970221e1b45af2b8 1ae415e05c204f4eaaa879b43af29b90 RX(theta₁₅) 8f76f794b6a84bca970221e1b45af2b8--1ae415e05c204f4eaaa879b43af29b90 ac28d3626f8e4abd82706086367a9165 1ae415e05c204f4eaaa879b43af29b90--ac28d3626f8e4abd82706086367a9165 858b2d6cf30b4ac79b5935d9ab5c183c ac28d3626f8e4abd82706086367a9165--858b2d6cf30b4ac79b5935d9ab5c183c c02f8aa18bb94a8f9f6da1e61e0f29c5 RX(theta₁₈) 858b2d6cf30b4ac79b5935d9ab5c183c--c02f8aa18bb94a8f9f6da1e61e0f29c5 f675b3db5b8b4716b94f596ff9b5108a RY(theta₂₁) c02f8aa18bb94a8f9f6da1e61e0f29c5--f675b3db5b8b4716b94f596ff9b5108a eaf66e9e19dd493ab7001c13f934bf89 RX(theta₂₄) f675b3db5b8b4716b94f596ff9b5108a--eaf66e9e19dd493ab7001c13f934bf89 161a1908a7b2444a9b3fa9d6ffd7a5ab eaf66e9e19dd493ab7001c13f934bf89--161a1908a7b2444a9b3fa9d6ffd7a5ab 465e42a4b81a4bde808865ebbda9c10e 161a1908a7b2444a9b3fa9d6ffd7a5ab--465e42a4b81a4bde808865ebbda9c10e e5220578747443779810eca3ec333b8b 465e42a4b81a4bde808865ebbda9c10e--e5220578747443779810eca3ec333b8b c8d3dcff369249a1bb25127952878276 427bf790ae194274af7a574f1c3af50b RX(2.0*acos(x)) b29236be863948c6b2fbbb5cccf0009c--427bf790ae194274af7a574f1c3af50b 03b60c53ab3f4494b351f338e084f872 2 a0aeb4e32b6543b58bdecff5b0bdc2d8 RX(theta₁) 427bf790ae194274af7a574f1c3af50b--a0aeb4e32b6543b58bdecff5b0bdc2d8 aec9385a546b467ea5aed4ee7343fac7 RY(theta₄) a0aeb4e32b6543b58bdecff5b0bdc2d8--aec9385a546b467ea5aed4ee7343fac7 9a6c0794f4b345959e5a8ca86845835b RX(theta₇) aec9385a546b467ea5aed4ee7343fac7--9a6c0794f4b345959e5a8ca86845835b 7f4ae9dd434e467b822a006a9615e654 X 9a6c0794f4b345959e5a8ca86845835b--7f4ae9dd434e467b822a006a9615e654 7f4ae9dd434e467b822a006a9615e654--4803df12a07f4f39b5a8bcefbea105bb 8826ce5b185a423bbaf27459a3d53f79 7f4ae9dd434e467b822a006a9615e654--8826ce5b185a423bbaf27459a3d53f79 98c49756afad4479928de40ab3c0868a RX(theta₁₀) 8826ce5b185a423bbaf27459a3d53f79--98c49756afad4479928de40ab3c0868a 6983dddba1cc4f99b60bcf14589e299f RY(theta₁₃) 98c49756afad4479928de40ab3c0868a--6983dddba1cc4f99b60bcf14589e299f fc1dc9e474ae4876a4b6172360c3fbcc RX(theta₁₆) 6983dddba1cc4f99b60bcf14589e299f--fc1dc9e474ae4876a4b6172360c3fbcc 987f54219d274f3fb610aa9233de851e X fc1dc9e474ae4876a4b6172360c3fbcc--987f54219d274f3fb610aa9233de851e 987f54219d274f3fb610aa9233de851e--ac28d3626f8e4abd82706086367a9165 fb25ee4a97c1424295091cd18ba24342 987f54219d274f3fb610aa9233de851e--fb25ee4a97c1424295091cd18ba24342 027114d7613b43e79dfcc7d464b18be1 RX(theta₁₉) fb25ee4a97c1424295091cd18ba24342--027114d7613b43e79dfcc7d464b18be1 a231fa01a5c74c38b3b04889cc0bb685 RY(theta₂₂) 027114d7613b43e79dfcc7d464b18be1--a231fa01a5c74c38b3b04889cc0bb685 8e47cb1355d94e3ead35d7d28ef6ff81 RX(theta₂₅) a231fa01a5c74c38b3b04889cc0bb685--8e47cb1355d94e3ead35d7d28ef6ff81 6f4f970b4f114fed8d8b6d09a8243b51 X 8e47cb1355d94e3ead35d7d28ef6ff81--6f4f970b4f114fed8d8b6d09a8243b51 6f4f970b4f114fed8d8b6d09a8243b51--161a1908a7b2444a9b3fa9d6ffd7a5ab 460e5c17ebe141f4891f10032fa8765a 6f4f970b4f114fed8d8b6d09a8243b51--460e5c17ebe141f4891f10032fa8765a 460e5c17ebe141f4891f10032fa8765a--c8d3dcff369249a1bb25127952878276 72658f5ab9ce421898b7507bbb0fd99c c019fdcfc06f48e1be4d8b074703ecbd RX(3.0*acos(x)) 03b60c53ab3f4494b351f338e084f872--c019fdcfc06f48e1be4d8b074703ecbd fac6ed8a109f44439e6f697871b015f3 RX(theta₂) c019fdcfc06f48e1be4d8b074703ecbd--fac6ed8a109f44439e6f697871b015f3 6ac4018eefb14112aba145feecf21854 RY(theta₅) fac6ed8a109f44439e6f697871b015f3--6ac4018eefb14112aba145feecf21854 4e945f7ad68b468191dbc597ba8bb0e9 RX(theta₈) 6ac4018eefb14112aba145feecf21854--4e945f7ad68b468191dbc597ba8bb0e9 9771e1b157894263b9748efd4d80cfe3 4e945f7ad68b468191dbc597ba8bb0e9--9771e1b157894263b9748efd4d80cfe3 3a44e829d71f49b2864adf26b9331175 X 9771e1b157894263b9748efd4d80cfe3--3a44e829d71f49b2864adf26b9331175 3a44e829d71f49b2864adf26b9331175--8826ce5b185a423bbaf27459a3d53f79 1fdc7b42d6574f9ab90529e25f50cfaf RX(theta₁₁) 3a44e829d71f49b2864adf26b9331175--1fdc7b42d6574f9ab90529e25f50cfaf d79691cd85e948269dca42da6c1e4225 RY(theta₁₄) 1fdc7b42d6574f9ab90529e25f50cfaf--d79691cd85e948269dca42da6c1e4225 3fc3f3d8c3ae4d0d9c85940534a27972 RX(theta₁₇) d79691cd85e948269dca42da6c1e4225--3fc3f3d8c3ae4d0d9c85940534a27972 4e8adb84ddd445c0b2aa56e233ad78a5 3fc3f3d8c3ae4d0d9c85940534a27972--4e8adb84ddd445c0b2aa56e233ad78a5 3df1b7803de946f6830a3aaf13122674 X 4e8adb84ddd445c0b2aa56e233ad78a5--3df1b7803de946f6830a3aaf13122674 3df1b7803de946f6830a3aaf13122674--fb25ee4a97c1424295091cd18ba24342 36235b4b6fba4f429c6c8cc74473a7c1 RX(theta₂₀) 3df1b7803de946f6830a3aaf13122674--36235b4b6fba4f429c6c8cc74473a7c1 71c6ac1860d64c148f4db75dfbecbf8a RY(theta₂₃) 36235b4b6fba4f429c6c8cc74473a7c1--71c6ac1860d64c148f4db75dfbecbf8a 4641618a196c46a3b2aeccc5def444bc RX(theta₂₆) 71c6ac1860d64c148f4db75dfbecbf8a--4641618a196c46a3b2aeccc5def444bc 33b7c6d4850b4e4bb3d6dd811afb0d19 4641618a196c46a3b2aeccc5def444bc--33b7c6d4850b4e4bb3d6dd811afb0d19 e5468e1475ea4694a1a541d97b8aa5de X 33b7c6d4850b4e4bb3d6dd811afb0d19--e5468e1475ea4694a1a541d97b8aa5de e5468e1475ea4694a1a541d97b8aa5de--460e5c17ebe141f4891f10032fa8765a e5468e1475ea4694a1a541d97b8aa5de--72658f5ab9ce421898b7507bbb0fd99c

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-08-21T11:48:32.487316 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-08-21T11:48:40.229724 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References