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_b294fee525064108bbd7ef8407c570f7 HEA cluster_2bc66ae633584aacac155830db4ce2ca Tower Chebyshev FM 180ea70846654630b9412603f4222a93 0 3d7777e68da944dbb6b45d3dc9e00a1b RX(1.0*acos(x)) 180ea70846654630b9412603f4222a93--3d7777e68da944dbb6b45d3dc9e00a1b 6863a4f442d84d9086f25756f89f6a58 1 7880595be9604bd7b9e81c36fb6d3c0d RX(theta₀) 3d7777e68da944dbb6b45d3dc9e00a1b--7880595be9604bd7b9e81c36fb6d3c0d df9c1675bc9644abbe80918c644fbd78 RY(theta₃) 7880595be9604bd7b9e81c36fb6d3c0d--df9c1675bc9644abbe80918c644fbd78 ebe95a7ab8744ec19e49ace928a3de3d RX(theta₆) df9c1675bc9644abbe80918c644fbd78--ebe95a7ab8744ec19e49ace928a3de3d 1ea470a05f5e441c9f1b57324ad19edb ebe95a7ab8744ec19e49ace928a3de3d--1ea470a05f5e441c9f1b57324ad19edb a23cc657efa34696bbff5b9e7ae75cf9 1ea470a05f5e441c9f1b57324ad19edb--a23cc657efa34696bbff5b9e7ae75cf9 24a7a7daf3c345c2abf0fb41e3d0fedd RX(theta₉) a23cc657efa34696bbff5b9e7ae75cf9--24a7a7daf3c345c2abf0fb41e3d0fedd 7b76d06613f54a46b4f68f6cf42fca56 RY(theta₁₂) 24a7a7daf3c345c2abf0fb41e3d0fedd--7b76d06613f54a46b4f68f6cf42fca56 285c4dbc7deb4c939c7c5d78a54bca02 RX(theta₁₅) 7b76d06613f54a46b4f68f6cf42fca56--285c4dbc7deb4c939c7c5d78a54bca02 cd6939731a7641eaa6433814b462c2ae 285c4dbc7deb4c939c7c5d78a54bca02--cd6939731a7641eaa6433814b462c2ae c882637315b6451083d60035298ba797 cd6939731a7641eaa6433814b462c2ae--c882637315b6451083d60035298ba797 a77287cdb1d34897beadbb2cd3e3b700 RX(theta₁₈) c882637315b6451083d60035298ba797--a77287cdb1d34897beadbb2cd3e3b700 be26f3509ce24fa6b5f1c60dc7113b7e RY(theta₂₁) a77287cdb1d34897beadbb2cd3e3b700--be26f3509ce24fa6b5f1c60dc7113b7e 7d124f85d4a740e289e8d5ca17a84c11 RX(theta₂₄) be26f3509ce24fa6b5f1c60dc7113b7e--7d124f85d4a740e289e8d5ca17a84c11 ebd6878be1fd4905a80afc7bde8f23c2 7d124f85d4a740e289e8d5ca17a84c11--ebd6878be1fd4905a80afc7bde8f23c2 82b8e6e6cc6f4e1baba4ee9e7d699c58 ebd6878be1fd4905a80afc7bde8f23c2--82b8e6e6cc6f4e1baba4ee9e7d699c58 5d802807e6f74eefa67caaff48acca15 82b8e6e6cc6f4e1baba4ee9e7d699c58--5d802807e6f74eefa67caaff48acca15 d8d075536f5c4f619ff7dad5c3afbff6 cc36919d88184e198269e9f8979650ca RX(2.0*acos(x)) 6863a4f442d84d9086f25756f89f6a58--cc36919d88184e198269e9f8979650ca 16ca4d416cee4ef7802487b60122bc37 2 acda98cffca74aa0b2fd1fcfd2cef94a RX(theta₁) cc36919d88184e198269e9f8979650ca--acda98cffca74aa0b2fd1fcfd2cef94a 6a27e66c683a46f1a973723648144471 RY(theta₄) acda98cffca74aa0b2fd1fcfd2cef94a--6a27e66c683a46f1a973723648144471 a3e717bdc38643f0aeac03c07ebcf895 RX(theta₇) 6a27e66c683a46f1a973723648144471--a3e717bdc38643f0aeac03c07ebcf895 2f94651a310e4aeab9fc6d1b80cd9c01 X a3e717bdc38643f0aeac03c07ebcf895--2f94651a310e4aeab9fc6d1b80cd9c01 2f94651a310e4aeab9fc6d1b80cd9c01--1ea470a05f5e441c9f1b57324ad19edb e884a7ac9dbc483090ce58d0866b96cb 2f94651a310e4aeab9fc6d1b80cd9c01--e884a7ac9dbc483090ce58d0866b96cb c46c7c2c1e06410aa58cb625e1cb737f RX(theta₁₀) e884a7ac9dbc483090ce58d0866b96cb--c46c7c2c1e06410aa58cb625e1cb737f 369602ea69fd4b63aacf4c25a8f9b4fc RY(theta₁₃) c46c7c2c1e06410aa58cb625e1cb737f--369602ea69fd4b63aacf4c25a8f9b4fc c9e6e0e448e949ebb4f1a7d8f42a6870 RX(theta₁₆) 369602ea69fd4b63aacf4c25a8f9b4fc--c9e6e0e448e949ebb4f1a7d8f42a6870 8e5ab49538dc4b729c16d0a48a8a5791 X c9e6e0e448e949ebb4f1a7d8f42a6870--8e5ab49538dc4b729c16d0a48a8a5791 8e5ab49538dc4b729c16d0a48a8a5791--cd6939731a7641eaa6433814b462c2ae 3838999ffe9643e48171846f416c41fa 8e5ab49538dc4b729c16d0a48a8a5791--3838999ffe9643e48171846f416c41fa 360ae2aad24f4b698c0cdef28c98e759 RX(theta₁₉) 3838999ffe9643e48171846f416c41fa--360ae2aad24f4b698c0cdef28c98e759 0f7216e22c78400d8a9aedd4abfa91a9 RY(theta₂₂) 360ae2aad24f4b698c0cdef28c98e759--0f7216e22c78400d8a9aedd4abfa91a9 cdb5ffaa981d4f81805c96799ba0be3d RX(theta₂₅) 0f7216e22c78400d8a9aedd4abfa91a9--cdb5ffaa981d4f81805c96799ba0be3d 34967b3dcb71498797cd0498b6f51815 X cdb5ffaa981d4f81805c96799ba0be3d--34967b3dcb71498797cd0498b6f51815 34967b3dcb71498797cd0498b6f51815--ebd6878be1fd4905a80afc7bde8f23c2 610c250f139442ed992d50c3919b8098 34967b3dcb71498797cd0498b6f51815--610c250f139442ed992d50c3919b8098 610c250f139442ed992d50c3919b8098--d8d075536f5c4f619ff7dad5c3afbff6 a71135a116ef43e984dcb3c429f330f7 daf580fbf0b7424d87ce81dd9dfc1dee RX(3.0*acos(x)) 16ca4d416cee4ef7802487b60122bc37--daf580fbf0b7424d87ce81dd9dfc1dee ad71b35aaa9a4c179621e11423749d95 RX(theta₂) daf580fbf0b7424d87ce81dd9dfc1dee--ad71b35aaa9a4c179621e11423749d95 034c046b8f5a4cd6973bc054a75daa02 RY(theta₅) ad71b35aaa9a4c179621e11423749d95--034c046b8f5a4cd6973bc054a75daa02 92b6eb93ee174fdabb4a56035d233480 RX(theta₈) 034c046b8f5a4cd6973bc054a75daa02--92b6eb93ee174fdabb4a56035d233480 e84ba259f14b4315898114d42f85b440 92b6eb93ee174fdabb4a56035d233480--e84ba259f14b4315898114d42f85b440 ec217ed288344d89ad2f22e6b2e5a928 X e84ba259f14b4315898114d42f85b440--ec217ed288344d89ad2f22e6b2e5a928 ec217ed288344d89ad2f22e6b2e5a928--e884a7ac9dbc483090ce58d0866b96cb f1cacfd27e9f4a83bc98f1a2ca0f00c8 RX(theta₁₁) ec217ed288344d89ad2f22e6b2e5a928--f1cacfd27e9f4a83bc98f1a2ca0f00c8 25c9299a49fd43d58c2eabce4d5dad03 RY(theta₁₄) f1cacfd27e9f4a83bc98f1a2ca0f00c8--25c9299a49fd43d58c2eabce4d5dad03 33c8713a790340e68b48a481b159db2b RX(theta₁₇) 25c9299a49fd43d58c2eabce4d5dad03--33c8713a790340e68b48a481b159db2b 715c71fe1e764a2db51987265b8f634d 33c8713a790340e68b48a481b159db2b--715c71fe1e764a2db51987265b8f634d b9e555bec5c6456782d443e2581780a2 X 715c71fe1e764a2db51987265b8f634d--b9e555bec5c6456782d443e2581780a2 b9e555bec5c6456782d443e2581780a2--3838999ffe9643e48171846f416c41fa e0c675909ea3440aa3fd87dfbd701a8b RX(theta₂₀) b9e555bec5c6456782d443e2581780a2--e0c675909ea3440aa3fd87dfbd701a8b a66dadd7536e4045a5541645a3139805 RY(theta₂₃) e0c675909ea3440aa3fd87dfbd701a8b--a66dadd7536e4045a5541645a3139805 4391cf54c67c485a9a15abd613cb4fd8 RX(theta₂₆) a66dadd7536e4045a5541645a3139805--4391cf54c67c485a9a15abd613cb4fd8 546fd9b9e0164471aef048d7d9f51ec6 4391cf54c67c485a9a15abd613cb4fd8--546fd9b9e0164471aef048d7d9f51ec6 a5e2b85e94a14cc9b766ecdcae680134 X 546fd9b9e0164471aef048d7d9f51ec6--a5e2b85e94a14cc9b766ecdcae680134 a5e2b85e94a14cc9b766ecdcae680134--610c250f139442ed992d50c3919b8098 a5e2b85e94a14cc9b766ecdcae680134--a71135a116ef43e984dcb3c429f330f7

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")
2025-01-08T18:02:33.109561 image/svg+xml Matplotlib v3.10.0, 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")
2025-01-08T18:02:40.272002 image/svg+xml Matplotlib v3.10.0, https://matplotlib.org/

References