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_b519d0243bae41ad9ccd65f4dde551c1 HEA cluster_bab6bf19bd504d3aafbcbc2aafb7b6a1 Tower Chebyshev FM ee68f39f6ab24ec9b7bb3e51790bc790 0 69aafd8d430e4e5790b5c124dfb6880e RX(1.0*acos(x)) ee68f39f6ab24ec9b7bb3e51790bc790--69aafd8d430e4e5790b5c124dfb6880e 2ff3a53c44c544319d7a11db9be99691 1 b1bf0f8b63354312a2faa55f627a5851 RX(theta₀) 69aafd8d430e4e5790b5c124dfb6880e--b1bf0f8b63354312a2faa55f627a5851 7a46b6fae03f4bfbbbb7222037091fe2 RY(theta₃) b1bf0f8b63354312a2faa55f627a5851--7a46b6fae03f4bfbbbb7222037091fe2 f0f823a2df4b440dbc784c464d079780 RX(theta₆) 7a46b6fae03f4bfbbbb7222037091fe2--f0f823a2df4b440dbc784c464d079780 aae66962812d46e2a10c8802066fdcc2 f0f823a2df4b440dbc784c464d079780--aae66962812d46e2a10c8802066fdcc2 aa94170570614d13893ee4614be0f895 aae66962812d46e2a10c8802066fdcc2--aa94170570614d13893ee4614be0f895 40655382412e4a28927e46ab91fa6260 RX(theta₉) aa94170570614d13893ee4614be0f895--40655382412e4a28927e46ab91fa6260 04e1a7f730d9437e9bcf167509dcee17 RY(theta₁₂) 40655382412e4a28927e46ab91fa6260--04e1a7f730d9437e9bcf167509dcee17 0a57222770414503ac8494125f7a0655 RX(theta₁₅) 04e1a7f730d9437e9bcf167509dcee17--0a57222770414503ac8494125f7a0655 4251b5bdf44a416caade488fad404bfd 0a57222770414503ac8494125f7a0655--4251b5bdf44a416caade488fad404bfd ff3f12891f7942d288c348a8fb8158e4 4251b5bdf44a416caade488fad404bfd--ff3f12891f7942d288c348a8fb8158e4 1faac853989846d59d59a0e39d482dae RX(theta₁₈) ff3f12891f7942d288c348a8fb8158e4--1faac853989846d59d59a0e39d482dae d64aa1e371e6415e9c423f7a0c7ddad0 RY(theta₂₁) 1faac853989846d59d59a0e39d482dae--d64aa1e371e6415e9c423f7a0c7ddad0 fb0a4da90fb444f88fdc5fad56b891d6 RX(theta₂₄) d64aa1e371e6415e9c423f7a0c7ddad0--fb0a4da90fb444f88fdc5fad56b891d6 fad9795951c14ef489447488d71f9a64 fb0a4da90fb444f88fdc5fad56b891d6--fad9795951c14ef489447488d71f9a64 7c41e015352c429486070967de59c558 fad9795951c14ef489447488d71f9a64--7c41e015352c429486070967de59c558 420798c2e0c14a079b6a0a75ab2908a7 7c41e015352c429486070967de59c558--420798c2e0c14a079b6a0a75ab2908a7 b3e9286a58604aaea5f058766134dd9d 9a28a71255c6415890d44cfa14d03713 RX(2.0*acos(x)) 2ff3a53c44c544319d7a11db9be99691--9a28a71255c6415890d44cfa14d03713 21006dbf2fc542c0b84ef095a39ba176 2 9554d1d050d94ade9910a7741182b426 RX(theta₁) 9a28a71255c6415890d44cfa14d03713--9554d1d050d94ade9910a7741182b426 f2f55b78c6854144a74bfec459c76ecd RY(theta₄) 9554d1d050d94ade9910a7741182b426--f2f55b78c6854144a74bfec459c76ecd fed25b8e903348508d405535cf8e0848 RX(theta₇) f2f55b78c6854144a74bfec459c76ecd--fed25b8e903348508d405535cf8e0848 6983e1abb2184fa2b04871d0c3321cad X fed25b8e903348508d405535cf8e0848--6983e1abb2184fa2b04871d0c3321cad 6983e1abb2184fa2b04871d0c3321cad--aae66962812d46e2a10c8802066fdcc2 e64b27b946934c4eaf2d3dc804a0ebaf 6983e1abb2184fa2b04871d0c3321cad--e64b27b946934c4eaf2d3dc804a0ebaf 71bb5e120f844dfe809352a7ae1e110e RX(theta₁₀) e64b27b946934c4eaf2d3dc804a0ebaf--71bb5e120f844dfe809352a7ae1e110e c949e75569194a65acfda83586e292c6 RY(theta₁₃) 71bb5e120f844dfe809352a7ae1e110e--c949e75569194a65acfda83586e292c6 b579a42fc2564db694972049a478de3a RX(theta₁₆) c949e75569194a65acfda83586e292c6--b579a42fc2564db694972049a478de3a 284baa9890644156a30d4b1fae81310e X b579a42fc2564db694972049a478de3a--284baa9890644156a30d4b1fae81310e 284baa9890644156a30d4b1fae81310e--4251b5bdf44a416caade488fad404bfd 344746d1aaa943f4998d6e616d4da027 284baa9890644156a30d4b1fae81310e--344746d1aaa943f4998d6e616d4da027 3c8c4745b9ba46d3abd884fbf8cd6975 RX(theta₁₉) 344746d1aaa943f4998d6e616d4da027--3c8c4745b9ba46d3abd884fbf8cd6975 45d47daefbbc435f86273870bb114192 RY(theta₂₂) 3c8c4745b9ba46d3abd884fbf8cd6975--45d47daefbbc435f86273870bb114192 3fd35b89099e48eb803c13f573adef12 RX(theta₂₅) 45d47daefbbc435f86273870bb114192--3fd35b89099e48eb803c13f573adef12 08e5583fb27641ac930b242b4dd6bea3 X 3fd35b89099e48eb803c13f573adef12--08e5583fb27641ac930b242b4dd6bea3 08e5583fb27641ac930b242b4dd6bea3--fad9795951c14ef489447488d71f9a64 f9676264e9a64603b0e03d3df8ba9c87 08e5583fb27641ac930b242b4dd6bea3--f9676264e9a64603b0e03d3df8ba9c87 f9676264e9a64603b0e03d3df8ba9c87--b3e9286a58604aaea5f058766134dd9d b056ce1fef004d57b9d9e434ef0e26aa b94c970a46e94997b74cd6f8f5281125 RX(3.0*acos(x)) 21006dbf2fc542c0b84ef095a39ba176--b94c970a46e94997b74cd6f8f5281125 0c8986a0f2de457fb08acec8a22c7623 RX(theta₂) b94c970a46e94997b74cd6f8f5281125--0c8986a0f2de457fb08acec8a22c7623 c9a02a83159a4c03b2042bd71a68be76 RY(theta₅) 0c8986a0f2de457fb08acec8a22c7623--c9a02a83159a4c03b2042bd71a68be76 5ae0df0193c547d2a310aa85508f8536 RX(theta₈) c9a02a83159a4c03b2042bd71a68be76--5ae0df0193c547d2a310aa85508f8536 216d38dcf0d6463badc29bff197dd0df 5ae0df0193c547d2a310aa85508f8536--216d38dcf0d6463badc29bff197dd0df c67cb6e6dfe548989cb0ae75dc60c4df X 216d38dcf0d6463badc29bff197dd0df--c67cb6e6dfe548989cb0ae75dc60c4df c67cb6e6dfe548989cb0ae75dc60c4df--e64b27b946934c4eaf2d3dc804a0ebaf 1cc88e6862d9413fa73bd61bc977d796 RX(theta₁₁) c67cb6e6dfe548989cb0ae75dc60c4df--1cc88e6862d9413fa73bd61bc977d796 32825278bf9541eeadbd2851c5747d1d RY(theta₁₄) 1cc88e6862d9413fa73bd61bc977d796--32825278bf9541eeadbd2851c5747d1d 5a23185608d44acdbb15724a4c3e5352 RX(theta₁₇) 32825278bf9541eeadbd2851c5747d1d--5a23185608d44acdbb15724a4c3e5352 0e17f0a5abdd45d68f07ed89e57c0405 5a23185608d44acdbb15724a4c3e5352--0e17f0a5abdd45d68f07ed89e57c0405 a44acfc870174e1b97f10c53cd18fe1e X 0e17f0a5abdd45d68f07ed89e57c0405--a44acfc870174e1b97f10c53cd18fe1e a44acfc870174e1b97f10c53cd18fe1e--344746d1aaa943f4998d6e616d4da027 8114c5049d4f478fb3c35ad023c7e21c RX(theta₂₀) a44acfc870174e1b97f10c53cd18fe1e--8114c5049d4f478fb3c35ad023c7e21c 91c435290da24ab583e4b675cf3dd46e RY(theta₂₃) 8114c5049d4f478fb3c35ad023c7e21c--91c435290da24ab583e4b675cf3dd46e b8d033a8507a4dab8ecb8d82ea4e213d RX(theta₂₆) 91c435290da24ab583e4b675cf3dd46e--b8d033a8507a4dab8ecb8d82ea4e213d 77ab753736c84ccd97e4a8dd0693e7bf b8d033a8507a4dab8ecb8d82ea4e213d--77ab753736c84ccd97e4a8dd0693e7bf 98a9b46c3d3649bdb8356e5919c36f9f X 77ab753736c84ccd97e4a8dd0693e7bf--98a9b46c3d3649bdb8356e5919c36f9f 98a9b46c3d3649bdb8356e5919c36f9f--f9676264e9a64603b0e03d3df8ba9c87 98a9b46c3d3649bdb8356e5919c36f9f--b056ce1fef004d57b9d9e434ef0e26aa

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-05-26T11:55:26.246616 image/svg+xml Matplotlib v3.10.3, 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-05-26T11:55:33.393458 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

References