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_dd2e03b31f484bd88d2945483c7c255f HEA cluster_c7d78a7aec114d4e9864125c69a0e0e5 Tower Chebyshev FM f372f0a2d93547958ddd891d8083e905 0 b5393bbe62dc42958d14a0f616570247 RX(1.0*acos(x)) f372f0a2d93547958ddd891d8083e905--b5393bbe62dc42958d14a0f616570247 72ac38690ac44fb3b3bd378d62778096 1 79d5580601084afeb492a03cd5de4e4a RX(theta₀) b5393bbe62dc42958d14a0f616570247--79d5580601084afeb492a03cd5de4e4a 8dbf57d9b31f4585aedb9b2f1814829a RY(theta₃) 79d5580601084afeb492a03cd5de4e4a--8dbf57d9b31f4585aedb9b2f1814829a 3cb54502bf574d1ebaa0be89d4398dc0 RX(theta₆) 8dbf57d9b31f4585aedb9b2f1814829a--3cb54502bf574d1ebaa0be89d4398dc0 8fa4ee6b3c224bf798326e843da323b1 3cb54502bf574d1ebaa0be89d4398dc0--8fa4ee6b3c224bf798326e843da323b1 6c2e6abf1c554174a02bbd13353f8b91 8fa4ee6b3c224bf798326e843da323b1--6c2e6abf1c554174a02bbd13353f8b91 078dfba5674143acaf82dec9c9c884b8 RX(theta₉) 6c2e6abf1c554174a02bbd13353f8b91--078dfba5674143acaf82dec9c9c884b8 5a68c9d4ec6d4798b629279ea47792a2 RY(theta₁₂) 078dfba5674143acaf82dec9c9c884b8--5a68c9d4ec6d4798b629279ea47792a2 314d3e5e54b14c8887540fd894df4088 RX(theta₁₅) 5a68c9d4ec6d4798b629279ea47792a2--314d3e5e54b14c8887540fd894df4088 6b74b6c780bd4bcbaededf682785debf 314d3e5e54b14c8887540fd894df4088--6b74b6c780bd4bcbaededf682785debf e8ccb70ec45c4a6d825f5a4b82a9a474 6b74b6c780bd4bcbaededf682785debf--e8ccb70ec45c4a6d825f5a4b82a9a474 2eb3c4b856544feeb7cd249a818c84d2 RX(theta₁₈) e8ccb70ec45c4a6d825f5a4b82a9a474--2eb3c4b856544feeb7cd249a818c84d2 7234c573737743c9ad5136631854b6f8 RY(theta₂₁) 2eb3c4b856544feeb7cd249a818c84d2--7234c573737743c9ad5136631854b6f8 17f88ebf3dcf448bb2af71caa3613c41 RX(theta₂₄) 7234c573737743c9ad5136631854b6f8--17f88ebf3dcf448bb2af71caa3613c41 a975f1b05d2046da945c160477f900a9 17f88ebf3dcf448bb2af71caa3613c41--a975f1b05d2046da945c160477f900a9 ad6e5451a0764897ae006ba6e4f62107 a975f1b05d2046da945c160477f900a9--ad6e5451a0764897ae006ba6e4f62107 cd68e724aa15432bb4d6d15a12beceec ad6e5451a0764897ae006ba6e4f62107--cd68e724aa15432bb4d6d15a12beceec d357656bbaf648bf9799b165c867e078 29b4f32f2d424fc1adb3b180f75d39a6 RX(2.0*acos(x)) 72ac38690ac44fb3b3bd378d62778096--29b4f32f2d424fc1adb3b180f75d39a6 475eb7bf44de4e2dacc428abf350e605 2 2b36018459d844f089b05a771d24f119 RX(theta₁) 29b4f32f2d424fc1adb3b180f75d39a6--2b36018459d844f089b05a771d24f119 a26761b75ba548d693b3f571ff80685e RY(theta₄) 2b36018459d844f089b05a771d24f119--a26761b75ba548d693b3f571ff80685e d5acc24b4b6a44928c98a48b15127fb7 RX(theta₇) a26761b75ba548d693b3f571ff80685e--d5acc24b4b6a44928c98a48b15127fb7 668022c2beb3492a809423f3f87dd90e X d5acc24b4b6a44928c98a48b15127fb7--668022c2beb3492a809423f3f87dd90e 668022c2beb3492a809423f3f87dd90e--8fa4ee6b3c224bf798326e843da323b1 160033b2e0a649aab0ff8061283006c3 668022c2beb3492a809423f3f87dd90e--160033b2e0a649aab0ff8061283006c3 6c90c668ac4745ba9d4f1bfbdbc7612e RX(theta₁₀) 160033b2e0a649aab0ff8061283006c3--6c90c668ac4745ba9d4f1bfbdbc7612e 1a9524895f0b4059bcc621fd7ab6a739 RY(theta₁₃) 6c90c668ac4745ba9d4f1bfbdbc7612e--1a9524895f0b4059bcc621fd7ab6a739 0a1103e622a04733894f4b0316afe72f RX(theta₁₆) 1a9524895f0b4059bcc621fd7ab6a739--0a1103e622a04733894f4b0316afe72f 890eddff96a04969846eb0519e0b019e X 0a1103e622a04733894f4b0316afe72f--890eddff96a04969846eb0519e0b019e 890eddff96a04969846eb0519e0b019e--6b74b6c780bd4bcbaededf682785debf 7651ac33686f4915a795f0585c162279 890eddff96a04969846eb0519e0b019e--7651ac33686f4915a795f0585c162279 39d0b3b1a184409fb6a6cf82489d5092 RX(theta₁₉) 7651ac33686f4915a795f0585c162279--39d0b3b1a184409fb6a6cf82489d5092 5ce57207af7943aca7a4395383fb52f3 RY(theta₂₂) 39d0b3b1a184409fb6a6cf82489d5092--5ce57207af7943aca7a4395383fb52f3 6d26c9ddb3c74740bd3c3c1f91d08a40 RX(theta₂₅) 5ce57207af7943aca7a4395383fb52f3--6d26c9ddb3c74740bd3c3c1f91d08a40 6083a5ba0b0a4f6cbb6b2c8a6dab6abd X 6d26c9ddb3c74740bd3c3c1f91d08a40--6083a5ba0b0a4f6cbb6b2c8a6dab6abd 6083a5ba0b0a4f6cbb6b2c8a6dab6abd--a975f1b05d2046da945c160477f900a9 87a59577da6a4236adff5016031d94e1 6083a5ba0b0a4f6cbb6b2c8a6dab6abd--87a59577da6a4236adff5016031d94e1 87a59577da6a4236adff5016031d94e1--d357656bbaf648bf9799b165c867e078 c5ace7dce6374ab1a690a5ed3d9f6475 6c2921fd585c4ac79c6a7134f3ebfa8e RX(3.0*acos(x)) 475eb7bf44de4e2dacc428abf350e605--6c2921fd585c4ac79c6a7134f3ebfa8e 6058447a7cf64914b38f5716cea5ce93 RX(theta₂) 6c2921fd585c4ac79c6a7134f3ebfa8e--6058447a7cf64914b38f5716cea5ce93 2512a674a40a4657a52c0c3950baf809 RY(theta₅) 6058447a7cf64914b38f5716cea5ce93--2512a674a40a4657a52c0c3950baf809 acec831f2227420da0280784ea44d0e3 RX(theta₈) 2512a674a40a4657a52c0c3950baf809--acec831f2227420da0280784ea44d0e3 268720586e744bd9afccf50f9f1660a1 acec831f2227420da0280784ea44d0e3--268720586e744bd9afccf50f9f1660a1 2915c52fb2e64b999bfe931f205dab26 X 268720586e744bd9afccf50f9f1660a1--2915c52fb2e64b999bfe931f205dab26 2915c52fb2e64b999bfe931f205dab26--160033b2e0a649aab0ff8061283006c3 8f72ab67246e42778fb990f02e55cc88 RX(theta₁₁) 2915c52fb2e64b999bfe931f205dab26--8f72ab67246e42778fb990f02e55cc88 178b784af4f74c8c8061baf8b7eea4f4 RY(theta₁₄) 8f72ab67246e42778fb990f02e55cc88--178b784af4f74c8c8061baf8b7eea4f4 3207f1ebc7f2448082f034bafaa50e54 RX(theta₁₇) 178b784af4f74c8c8061baf8b7eea4f4--3207f1ebc7f2448082f034bafaa50e54 4a077326e9f74330a441dee3972cbedb 3207f1ebc7f2448082f034bafaa50e54--4a077326e9f74330a441dee3972cbedb dca9c6a71b1545b9bb3532597f1bd715 X 4a077326e9f74330a441dee3972cbedb--dca9c6a71b1545b9bb3532597f1bd715 dca9c6a71b1545b9bb3532597f1bd715--7651ac33686f4915a795f0585c162279 1258e130921541e6a726eeb460fdf304 RX(theta₂₀) dca9c6a71b1545b9bb3532597f1bd715--1258e130921541e6a726eeb460fdf304 f0307b50ea66414f8e752335aaad77c6 RY(theta₂₃) 1258e130921541e6a726eeb460fdf304--f0307b50ea66414f8e752335aaad77c6 75a5537e1d454dccb007da086728f630 RX(theta₂₆) f0307b50ea66414f8e752335aaad77c6--75a5537e1d454dccb007da086728f630 3dc6b3a296ed4b05b11d0bad847eaf13 75a5537e1d454dccb007da086728f630--3dc6b3a296ed4b05b11d0bad847eaf13 2e5902330ceb4eeaa10009586fbe469a X 3dc6b3a296ed4b05b11d0bad847eaf13--2e5902330ceb4eeaa10009586fbe469a 2e5902330ceb4eeaa10009586fbe469a--87a59577da6a4236adff5016031d94e1 2e5902330ceb4eeaa10009586fbe469a--c5ace7dce6374ab1a690a5ed3d9f6475

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-02-07T10:22:21.012028 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-02-07T10:22:27.887638 image/svg+xml Matplotlib v3.10.0, https://matplotlib.org/

References