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:

dfdx=5×(4x3+x22x12),f(0)=0

It admits an exact solution:

f(x)=5×(x4+13x3x212x)

Our goal will be to find this solution for x[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_87780c48c7b944a48cbefe8d137bae6e HEA cluster_7e0e11e75015471c841e633e64dedf38 Tower Chebyshev FM 3033b381ad5941b79a8f1ce6f1613343 0 9d1b16c893b1451d95abc0269a4f528a RX(1.0*acos(x)) 3033b381ad5941b79a8f1ce6f1613343--9d1b16c893b1451d95abc0269a4f528a 4cfe1ae75ec14a2f970e1ae80a1ce1dd 1 e3cfc0ef9e44479aadf32a812b189333 RX(theta₀) 9d1b16c893b1451d95abc0269a4f528a--e3cfc0ef9e44479aadf32a812b189333 7a1cf26f1bb64e98b1c3afe3c54a97de RY(theta₃) e3cfc0ef9e44479aadf32a812b189333--7a1cf26f1bb64e98b1c3afe3c54a97de 2a8ab0babc5e46f5aab95a36b8e2ba23 RX(theta₆) 7a1cf26f1bb64e98b1c3afe3c54a97de--2a8ab0babc5e46f5aab95a36b8e2ba23 6287e0f9d1584012948239b41aa6530b 2a8ab0babc5e46f5aab95a36b8e2ba23--6287e0f9d1584012948239b41aa6530b f43daaa33c5f4978b479ceacdb592809 6287e0f9d1584012948239b41aa6530b--f43daaa33c5f4978b479ceacdb592809 298fc064727745038ac0dd00b2b5c214 RX(theta₉) f43daaa33c5f4978b479ceacdb592809--298fc064727745038ac0dd00b2b5c214 ebb8d737782c4a298376d3bb8a7a793b RY(theta₁₂) 298fc064727745038ac0dd00b2b5c214--ebb8d737782c4a298376d3bb8a7a793b 7a5e1af96df948aa81a896511c3e1638 RX(theta₁₅) ebb8d737782c4a298376d3bb8a7a793b--7a5e1af96df948aa81a896511c3e1638 2c2229d1c84f49e19ef0d7432cf1742f 7a5e1af96df948aa81a896511c3e1638--2c2229d1c84f49e19ef0d7432cf1742f 9ce9010336b742eea32c0336ce947466 2c2229d1c84f49e19ef0d7432cf1742f--9ce9010336b742eea32c0336ce947466 2c90e6109739410794cbd54f01a1aeb5 RX(theta₁₈) 9ce9010336b742eea32c0336ce947466--2c90e6109739410794cbd54f01a1aeb5 e253f6a21ec1439e82d80165f54f127b RY(theta₂₁) 2c90e6109739410794cbd54f01a1aeb5--e253f6a21ec1439e82d80165f54f127b 483827643cf54910bdedc028ec279eb7 RX(theta₂₄) e253f6a21ec1439e82d80165f54f127b--483827643cf54910bdedc028ec279eb7 66fb16ac242e405787805e8fd869d83f 483827643cf54910bdedc028ec279eb7--66fb16ac242e405787805e8fd869d83f 45b3d58a04ce496e99c17d98f48ccbb8 66fb16ac242e405787805e8fd869d83f--45b3d58a04ce496e99c17d98f48ccbb8 3a8a9342ba7045b7956b019e83a762c8 45b3d58a04ce496e99c17d98f48ccbb8--3a8a9342ba7045b7956b019e83a762c8 3cdb84818de049d499edb237db4a3d6c 9d1672691a3441c0b5fb0cd86f776459 RX(2.0*acos(x)) 4cfe1ae75ec14a2f970e1ae80a1ce1dd--9d1672691a3441c0b5fb0cd86f776459 6ccd58178a4c423181dadec29ad060a6 2 f018ab30ed404248b2ae2f2cbe964947 RX(theta₁) 9d1672691a3441c0b5fb0cd86f776459--f018ab30ed404248b2ae2f2cbe964947 f1a56dab569d4aeab4a02d718fece8b1 RY(theta₄) f018ab30ed404248b2ae2f2cbe964947--f1a56dab569d4aeab4a02d718fece8b1 5fb2ced9da394997acb47c177b7d5591 RX(theta₇) f1a56dab569d4aeab4a02d718fece8b1--5fb2ced9da394997acb47c177b7d5591 9fefbe963ad74350b0e7f8403b92daac X 5fb2ced9da394997acb47c177b7d5591--9fefbe963ad74350b0e7f8403b92daac 9fefbe963ad74350b0e7f8403b92daac--6287e0f9d1584012948239b41aa6530b 837c3a02407a483bbd2e99249ead160f 9fefbe963ad74350b0e7f8403b92daac--837c3a02407a483bbd2e99249ead160f 7ace342d31724fe5932fda151878201c RX(theta₁₀) 837c3a02407a483bbd2e99249ead160f--7ace342d31724fe5932fda151878201c 743f0e0b94ce4238b3667456445cb28b RY(theta₁₃) 7ace342d31724fe5932fda151878201c--743f0e0b94ce4238b3667456445cb28b ccf2fda3a6c34ff49d5113500efb1d4e RX(theta₁₆) 743f0e0b94ce4238b3667456445cb28b--ccf2fda3a6c34ff49d5113500efb1d4e dd58c7002fd14bfc96aa3ea2028deba5 X ccf2fda3a6c34ff49d5113500efb1d4e--dd58c7002fd14bfc96aa3ea2028deba5 dd58c7002fd14bfc96aa3ea2028deba5--2c2229d1c84f49e19ef0d7432cf1742f a7812742acdc4c04af4c5619f6c8d8f0 dd58c7002fd14bfc96aa3ea2028deba5--a7812742acdc4c04af4c5619f6c8d8f0 0e93610151074042be1829ca9711156f RX(theta₁₉) a7812742acdc4c04af4c5619f6c8d8f0--0e93610151074042be1829ca9711156f 006d1dce179b4d1aa34acc425c96901b RY(theta₂₂) 0e93610151074042be1829ca9711156f--006d1dce179b4d1aa34acc425c96901b 643c61abef544d4c8363b56c88c2d2a6 RX(theta₂₅) 006d1dce179b4d1aa34acc425c96901b--643c61abef544d4c8363b56c88c2d2a6 4abced65b59b4ca39a53110402b5ea25 X 643c61abef544d4c8363b56c88c2d2a6--4abced65b59b4ca39a53110402b5ea25 4abced65b59b4ca39a53110402b5ea25--66fb16ac242e405787805e8fd869d83f 5bc74135a5014afbb0ccc1bdc46345fc 4abced65b59b4ca39a53110402b5ea25--5bc74135a5014afbb0ccc1bdc46345fc 5bc74135a5014afbb0ccc1bdc46345fc--3cdb84818de049d499edb237db4a3d6c 64070dca64724a75b81ab1a18c54cc6e de250a90fd7a4ed89125d171925f9558 RX(3.0*acos(x)) 6ccd58178a4c423181dadec29ad060a6--de250a90fd7a4ed89125d171925f9558 47b33a9da7fa4750b6db908a63978f36 RX(theta₂) de250a90fd7a4ed89125d171925f9558--47b33a9da7fa4750b6db908a63978f36 22b1f22b3a604fe1b1a93023eaa6954f RY(theta₅) 47b33a9da7fa4750b6db908a63978f36--22b1f22b3a604fe1b1a93023eaa6954f 1c123e3385e34156bc5d1641e91acefe RX(theta₈) 22b1f22b3a604fe1b1a93023eaa6954f--1c123e3385e34156bc5d1641e91acefe e288a914468c43c0b441153c2199f1fb 1c123e3385e34156bc5d1641e91acefe--e288a914468c43c0b441153c2199f1fb 6af8dfd51a35447396fa8129eb5d9987 X e288a914468c43c0b441153c2199f1fb--6af8dfd51a35447396fa8129eb5d9987 6af8dfd51a35447396fa8129eb5d9987--837c3a02407a483bbd2e99249ead160f d545298f104540a7afec05e3742fc3b6 RX(theta₁₁) 6af8dfd51a35447396fa8129eb5d9987--d545298f104540a7afec05e3742fc3b6 4a97ecb39abf4c17a19b716fd77722e2 RY(theta₁₄) d545298f104540a7afec05e3742fc3b6--4a97ecb39abf4c17a19b716fd77722e2 a732a525aea549afadbee3ef504e42c9 RX(theta₁₇) 4a97ecb39abf4c17a19b716fd77722e2--a732a525aea549afadbee3ef504e42c9 8e7e22b920dd4621948001239ca08804 a732a525aea549afadbee3ef504e42c9--8e7e22b920dd4621948001239ca08804 34ad093ce19742e2804643f376190361 X 8e7e22b920dd4621948001239ca08804--34ad093ce19742e2804643f376190361 34ad093ce19742e2804643f376190361--a7812742acdc4c04af4c5619f6c8d8f0 226ba70e8e16468aaa25462c68036a6c RX(theta₂₀) 34ad093ce19742e2804643f376190361--226ba70e8e16468aaa25462c68036a6c 7a77ecb24e534817bd8de5ba490d89d2 RY(theta₂₃) 226ba70e8e16468aaa25462c68036a6c--7a77ecb24e534817bd8de5ba490d89d2 e6ab71a9b9c24adeb1b17f8cb0d33ec2 RX(theta₂₆) 7a77ecb24e534817bd8de5ba490d89d2--e6ab71a9b9c24adeb1b17f8cb0d33ec2 742b9ad1eefc4438a70353f3c023927a e6ab71a9b9c24adeb1b17f8cb0d33ec2--742b9ad1eefc4438a70353f3c023927a a4eb93f612c14df8977a193e100da83c X 742b9ad1eefc4438a70353f3c023927a--a4eb93f612c14df8977a193e100da83c a4eb93f612c14df8977a193e100da83c--5bc74135a5014afbb0ccc1bdc46345fc a4eb93f612c14df8977a193e100da83c--64070dca64724a75b81ab1a18c54cc6e

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[0.99,0.99] since we are using a Chebyshev feature map, and derivative of 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-04-04T13:35:02.807858 image/svg+xml Matplotlib v3.10.1, 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-04-04T13:35:10.687413 image/svg+xml Matplotlib v3.10.1, https://matplotlib.org/

References