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_4d6c606d3eee42babc0cfbd4fbbb4437 HEA cluster_9fa5a3e47bb94b63bd1721fbc6ade183 Tower Chebyshev FM a8c5f8ed2c814a1e8bc27f3f94372cab 0 e74b9fd213df455ba4e1b47ba31ea35c RX(1.0*acos(x)) a8c5f8ed2c814a1e8bc27f3f94372cab--e74b9fd213df455ba4e1b47ba31ea35c fae8175b4e5042bc93629f7c78f07128 1 547af50ad8c74a97b4cae09b50b84588 RX(theta₀) e74b9fd213df455ba4e1b47ba31ea35c--547af50ad8c74a97b4cae09b50b84588 edd49830ca984eb4b36c1b087ad5cad1 RY(theta₃) 547af50ad8c74a97b4cae09b50b84588--edd49830ca984eb4b36c1b087ad5cad1 5b43c5c19c3b493f9453f9e3f465f442 RX(theta₆) edd49830ca984eb4b36c1b087ad5cad1--5b43c5c19c3b493f9453f9e3f465f442 e549b7d599164aab85d63da0ed775eb3 5b43c5c19c3b493f9453f9e3f465f442--e549b7d599164aab85d63da0ed775eb3 75939e60076b4e7ea1cbb71e01d2b11e e549b7d599164aab85d63da0ed775eb3--75939e60076b4e7ea1cbb71e01d2b11e c6c768f0901448ea81d4c7860f0251f5 RX(theta₉) 75939e60076b4e7ea1cbb71e01d2b11e--c6c768f0901448ea81d4c7860f0251f5 12e9e4c3e35a4957acc84df1da31a57a RY(theta₁₂) c6c768f0901448ea81d4c7860f0251f5--12e9e4c3e35a4957acc84df1da31a57a b465e73c0ce741f2bd1fc4d8a6ce44fb RX(theta₁₅) 12e9e4c3e35a4957acc84df1da31a57a--b465e73c0ce741f2bd1fc4d8a6ce44fb b6453c8b75c745a88dfd1d50871ed42e b465e73c0ce741f2bd1fc4d8a6ce44fb--b6453c8b75c745a88dfd1d50871ed42e d55c94a74aa941309f9be653e8acfc21 b6453c8b75c745a88dfd1d50871ed42e--d55c94a74aa941309f9be653e8acfc21 e7ed70ea0c864aff9a3848c611aee8f0 RX(theta₁₈) d55c94a74aa941309f9be653e8acfc21--e7ed70ea0c864aff9a3848c611aee8f0 24f058cf6d2840578dec9e18da5b7969 RY(theta₂₁) e7ed70ea0c864aff9a3848c611aee8f0--24f058cf6d2840578dec9e18da5b7969 8b674027fd4c4a688d5a9512f7141355 RX(theta₂₄) 24f058cf6d2840578dec9e18da5b7969--8b674027fd4c4a688d5a9512f7141355 296f647181824b8999bf866002b8a54e 8b674027fd4c4a688d5a9512f7141355--296f647181824b8999bf866002b8a54e 7ce9097849074816b204807104cb1db1 296f647181824b8999bf866002b8a54e--7ce9097849074816b204807104cb1db1 78e0ca24d4374c5f9fabd669b805c109 7ce9097849074816b204807104cb1db1--78e0ca24d4374c5f9fabd669b805c109 2da5c97012324461bd43517001fbbc9a 080e32b6953b481a947d583f22efc500 RX(2.0*acos(x)) fae8175b4e5042bc93629f7c78f07128--080e32b6953b481a947d583f22efc500 f2d51a26220b443c8560a89ac3ad8fad 2 6b3ba051e2bf4f5e96da402cdc02d071 RX(theta₁) 080e32b6953b481a947d583f22efc500--6b3ba051e2bf4f5e96da402cdc02d071 872a89dfd2064e83acb44f0388ed7960 RY(theta₄) 6b3ba051e2bf4f5e96da402cdc02d071--872a89dfd2064e83acb44f0388ed7960 4436895eaf7f407482c918dbdcfb1211 RX(theta₇) 872a89dfd2064e83acb44f0388ed7960--4436895eaf7f407482c918dbdcfb1211 51ac19ab2dff418d99b6f0e0ec959adf X 4436895eaf7f407482c918dbdcfb1211--51ac19ab2dff418d99b6f0e0ec959adf 51ac19ab2dff418d99b6f0e0ec959adf--e549b7d599164aab85d63da0ed775eb3 eb284af7925c40b69e37498245dacc3c 51ac19ab2dff418d99b6f0e0ec959adf--eb284af7925c40b69e37498245dacc3c 6aa3a6e144264b6693ff08f695bfde32 RX(theta₁₀) eb284af7925c40b69e37498245dacc3c--6aa3a6e144264b6693ff08f695bfde32 b3e6899d97eb4e219979fc2f5aaa3cc7 RY(theta₁₃) 6aa3a6e144264b6693ff08f695bfde32--b3e6899d97eb4e219979fc2f5aaa3cc7 91019edb6937424592fd016ad871c360 RX(theta₁₆) b3e6899d97eb4e219979fc2f5aaa3cc7--91019edb6937424592fd016ad871c360 0f90564bedf94a83a323b038f4ffe5ca X 91019edb6937424592fd016ad871c360--0f90564bedf94a83a323b038f4ffe5ca 0f90564bedf94a83a323b038f4ffe5ca--b6453c8b75c745a88dfd1d50871ed42e 4e78873c72f64554b7d8aa2bc8a80407 0f90564bedf94a83a323b038f4ffe5ca--4e78873c72f64554b7d8aa2bc8a80407 e700963b571b45419dc5b1d949e84c15 RX(theta₁₉) 4e78873c72f64554b7d8aa2bc8a80407--e700963b571b45419dc5b1d949e84c15 8c455980b72a439ca569c41014685fa5 RY(theta₂₂) e700963b571b45419dc5b1d949e84c15--8c455980b72a439ca569c41014685fa5 419441eda7104f7f8088bc220cd159da RX(theta₂₅) 8c455980b72a439ca569c41014685fa5--419441eda7104f7f8088bc220cd159da 930fce1e70af4bdea52c399288292a33 X 419441eda7104f7f8088bc220cd159da--930fce1e70af4bdea52c399288292a33 930fce1e70af4bdea52c399288292a33--296f647181824b8999bf866002b8a54e 212a2e4cbbbf43acb22dfd9301d4dd5c 930fce1e70af4bdea52c399288292a33--212a2e4cbbbf43acb22dfd9301d4dd5c 212a2e4cbbbf43acb22dfd9301d4dd5c--2da5c97012324461bd43517001fbbc9a 69b20a5bde6a48719b74a4a7c16dff69 b589849963a5431d8cabe0e0a0ef38a1 RX(3.0*acos(x)) f2d51a26220b443c8560a89ac3ad8fad--b589849963a5431d8cabe0e0a0ef38a1 d7536b431f54416296c8f9a735beffa0 RX(theta₂) b589849963a5431d8cabe0e0a0ef38a1--d7536b431f54416296c8f9a735beffa0 d934311c3c4a4848986095519dc33392 RY(theta₅) d7536b431f54416296c8f9a735beffa0--d934311c3c4a4848986095519dc33392 c64abd9a6c1545f196d1452fd641fa1c RX(theta₈) d934311c3c4a4848986095519dc33392--c64abd9a6c1545f196d1452fd641fa1c e270957ffaed486d9ba467e6a484f8d8 c64abd9a6c1545f196d1452fd641fa1c--e270957ffaed486d9ba467e6a484f8d8 26be5af1a82a4869a90f7a6c7ca3d6f4 X e270957ffaed486d9ba467e6a484f8d8--26be5af1a82a4869a90f7a6c7ca3d6f4 26be5af1a82a4869a90f7a6c7ca3d6f4--eb284af7925c40b69e37498245dacc3c 10c20d9c10064806b8baa441cf4e4013 RX(theta₁₁) 26be5af1a82a4869a90f7a6c7ca3d6f4--10c20d9c10064806b8baa441cf4e4013 e0dc4551f4e042b881caca21c8c9ef7e RY(theta₁₄) 10c20d9c10064806b8baa441cf4e4013--e0dc4551f4e042b881caca21c8c9ef7e 38fd9d1fb8b9446cb7b935dbf439fba3 RX(theta₁₇) e0dc4551f4e042b881caca21c8c9ef7e--38fd9d1fb8b9446cb7b935dbf439fba3 4003c3a4caa84538ba2a68916d18b1bf 38fd9d1fb8b9446cb7b935dbf439fba3--4003c3a4caa84538ba2a68916d18b1bf 3f0227cad06046eb966988dc8ad54a35 X 4003c3a4caa84538ba2a68916d18b1bf--3f0227cad06046eb966988dc8ad54a35 3f0227cad06046eb966988dc8ad54a35--4e78873c72f64554b7d8aa2bc8a80407 b8f212921b6143ed95fef535196d58c8 RX(theta₂₀) 3f0227cad06046eb966988dc8ad54a35--b8f212921b6143ed95fef535196d58c8 4cead6e52ea64f25ad531ea161d6aaa0 RY(theta₂₃) b8f212921b6143ed95fef535196d58c8--4cead6e52ea64f25ad531ea161d6aaa0 6630fd69bef54ac39446a1a5f79a6897 RX(theta₂₆) 4cead6e52ea64f25ad531ea161d6aaa0--6630fd69bef54ac39446a1a5f79a6897 c121f2b30ef942b784ed2e6fea2501fe 6630fd69bef54ac39446a1a5f79a6897--c121f2b30ef942b784ed2e6fea2501fe 3963645066d440b19fe8c98829823138 X c121f2b30ef942b784ed2e6fea2501fe--3963645066d440b19fe8c98829823138 3963645066d440b19fe8c98829823138--212a2e4cbbbf43acb22dfd9301d4dd5c 3963645066d440b19fe8c98829823138--69b20a5bde6a48719b74a4a7c16dff69

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-19T12:06:53.826146 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-19T12:07:01.847965 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References