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_c9aa0b9c81cb4ceaa7f4090d4ca0be51 HEA cluster_7f0b69c467f3461990f4a08612d6a7fb Tower Chebyshev FM 6aa0699223a14681b818e1d666a523d2 0 bf67e2a9d58446bea3144dca8fb132b8 RX(1.0*acos(x)) 6aa0699223a14681b818e1d666a523d2--bf67e2a9d58446bea3144dca8fb132b8 a3384e9a1c244545a8b873b2a1388f67 1 4b056c37dfc74a66b95340a6bfb33ba8 RX(theta₀) bf67e2a9d58446bea3144dca8fb132b8--4b056c37dfc74a66b95340a6bfb33ba8 e78928fec48840fb84d776c33947a6eb RY(theta₃) 4b056c37dfc74a66b95340a6bfb33ba8--e78928fec48840fb84d776c33947a6eb 5a81713aca774861ba9093e4049d7cc8 RX(theta₆) e78928fec48840fb84d776c33947a6eb--5a81713aca774861ba9093e4049d7cc8 cecf97cc6b5f47b0826b023b7ab2849f 5a81713aca774861ba9093e4049d7cc8--cecf97cc6b5f47b0826b023b7ab2849f 7ed678f0fd654043a7d4a49934d8127b cecf97cc6b5f47b0826b023b7ab2849f--7ed678f0fd654043a7d4a49934d8127b aba3b5585f224c56b467cf47cbae17c5 RX(theta₉) 7ed678f0fd654043a7d4a49934d8127b--aba3b5585f224c56b467cf47cbae17c5 cde064cb554c480baaaa1de517cd313b RY(theta₁₂) aba3b5585f224c56b467cf47cbae17c5--cde064cb554c480baaaa1de517cd313b 94cc89a95e4f48ff8e110110c5ed49dc RX(theta₁₅) cde064cb554c480baaaa1de517cd313b--94cc89a95e4f48ff8e110110c5ed49dc 9f8dd9ace4ba494fad1f2c96f639168a 94cc89a95e4f48ff8e110110c5ed49dc--9f8dd9ace4ba494fad1f2c96f639168a 77e017dabe5b4581802b3db76d5694e0 9f8dd9ace4ba494fad1f2c96f639168a--77e017dabe5b4581802b3db76d5694e0 d3285e2ac80646f39bd80b77eafb76ac RX(theta₁₈) 77e017dabe5b4581802b3db76d5694e0--d3285e2ac80646f39bd80b77eafb76ac 5ea17108ae3142f1a894123a24d6fe95 RY(theta₂₁) d3285e2ac80646f39bd80b77eafb76ac--5ea17108ae3142f1a894123a24d6fe95 16c61362eec948b1a727d581a2b1ce3f RX(theta₂₄) 5ea17108ae3142f1a894123a24d6fe95--16c61362eec948b1a727d581a2b1ce3f 766998eaa4144b318c839d363a9dbff6 16c61362eec948b1a727d581a2b1ce3f--766998eaa4144b318c839d363a9dbff6 43072707de4048d18442c2c2cc1737d0 766998eaa4144b318c839d363a9dbff6--43072707de4048d18442c2c2cc1737d0 7c54ecc096fb43e19fa843f3a74d1404 43072707de4048d18442c2c2cc1737d0--7c54ecc096fb43e19fa843f3a74d1404 890dc68a1c354dfdb8e7bace2b44f56a 19778ed6fd524c54889347b26626304e RX(2.0*acos(x)) a3384e9a1c244545a8b873b2a1388f67--19778ed6fd524c54889347b26626304e bd3ecc62f42548679287422202190a40 2 6f279d47752e428691f0359054eef991 RX(theta₁) 19778ed6fd524c54889347b26626304e--6f279d47752e428691f0359054eef991 9b5ca4e8dec04bae8199a5b4e91436ab RY(theta₄) 6f279d47752e428691f0359054eef991--9b5ca4e8dec04bae8199a5b4e91436ab 57083cb38bd1496f979b4462b9db331a RX(theta₇) 9b5ca4e8dec04bae8199a5b4e91436ab--57083cb38bd1496f979b4462b9db331a a3aebf4300b04966bc37838401f59a8d X 57083cb38bd1496f979b4462b9db331a--a3aebf4300b04966bc37838401f59a8d a3aebf4300b04966bc37838401f59a8d--cecf97cc6b5f47b0826b023b7ab2849f 8ae57c79b09b4e5f80ad78e417e98ad8 a3aebf4300b04966bc37838401f59a8d--8ae57c79b09b4e5f80ad78e417e98ad8 979a09671fa3428dad75d81b2d96705c RX(theta₁₀) 8ae57c79b09b4e5f80ad78e417e98ad8--979a09671fa3428dad75d81b2d96705c b3409530380b48f284adf6aada214649 RY(theta₁₃) 979a09671fa3428dad75d81b2d96705c--b3409530380b48f284adf6aada214649 d75ac42c0f5b4c638eac241d2e4357db RX(theta₁₆) b3409530380b48f284adf6aada214649--d75ac42c0f5b4c638eac241d2e4357db 45d2cc8de0d740f29231800837726ef3 X d75ac42c0f5b4c638eac241d2e4357db--45d2cc8de0d740f29231800837726ef3 45d2cc8de0d740f29231800837726ef3--9f8dd9ace4ba494fad1f2c96f639168a 0f6979527fd8466abceabee095cf9404 45d2cc8de0d740f29231800837726ef3--0f6979527fd8466abceabee095cf9404 ee871f80068640028adae55f772fd9bf RX(theta₁₉) 0f6979527fd8466abceabee095cf9404--ee871f80068640028adae55f772fd9bf 8f0968965f7e4f93a1eb315f4608c525 RY(theta₂₂) ee871f80068640028adae55f772fd9bf--8f0968965f7e4f93a1eb315f4608c525 74b174a871264452a322654e1808c9c8 RX(theta₂₅) 8f0968965f7e4f93a1eb315f4608c525--74b174a871264452a322654e1808c9c8 7d65bfa0dc81414d84b893ad5f8b2b4e X 74b174a871264452a322654e1808c9c8--7d65bfa0dc81414d84b893ad5f8b2b4e 7d65bfa0dc81414d84b893ad5f8b2b4e--766998eaa4144b318c839d363a9dbff6 b70722d81219424c856a8de344a1484b 7d65bfa0dc81414d84b893ad5f8b2b4e--b70722d81219424c856a8de344a1484b b70722d81219424c856a8de344a1484b--890dc68a1c354dfdb8e7bace2b44f56a 4ca1a54532894600993f431874452973 352f3bd475464775acebb1bb1bb6389e RX(3.0*acos(x)) bd3ecc62f42548679287422202190a40--352f3bd475464775acebb1bb1bb6389e c13580e4bb1643e49de1ff4f146d1213 RX(theta₂) 352f3bd475464775acebb1bb1bb6389e--c13580e4bb1643e49de1ff4f146d1213 641bf1755f714221ae679d2bf760e52a RY(theta₅) c13580e4bb1643e49de1ff4f146d1213--641bf1755f714221ae679d2bf760e52a 8c97d36aa72248f282b4d08637a67f94 RX(theta₈) 641bf1755f714221ae679d2bf760e52a--8c97d36aa72248f282b4d08637a67f94 dedcc77b1ecb4aa38e83c0fe7a48499e 8c97d36aa72248f282b4d08637a67f94--dedcc77b1ecb4aa38e83c0fe7a48499e ce0e70352d0c47bdabf843daf2f59cfd X dedcc77b1ecb4aa38e83c0fe7a48499e--ce0e70352d0c47bdabf843daf2f59cfd ce0e70352d0c47bdabf843daf2f59cfd--8ae57c79b09b4e5f80ad78e417e98ad8 6226d60d631f4d09a93230e859fd4b18 RX(theta₁₁) ce0e70352d0c47bdabf843daf2f59cfd--6226d60d631f4d09a93230e859fd4b18 9964c4eadb4b48949a549501486a5231 RY(theta₁₄) 6226d60d631f4d09a93230e859fd4b18--9964c4eadb4b48949a549501486a5231 b681847916e34d45987b2d6a34078982 RX(theta₁₇) 9964c4eadb4b48949a549501486a5231--b681847916e34d45987b2d6a34078982 0d75692f14eb4e7082d6578079aa410b b681847916e34d45987b2d6a34078982--0d75692f14eb4e7082d6578079aa410b af5fd4fad90f425da90b0278922fbfb2 X 0d75692f14eb4e7082d6578079aa410b--af5fd4fad90f425da90b0278922fbfb2 af5fd4fad90f425da90b0278922fbfb2--0f6979527fd8466abceabee095cf9404 cd90e9613ca5441c87a62983ce1fdf53 RX(theta₂₀) af5fd4fad90f425da90b0278922fbfb2--cd90e9613ca5441c87a62983ce1fdf53 0be8d809bcdf4eeab10d4fd8cbc69a8a RY(theta₂₃) cd90e9613ca5441c87a62983ce1fdf53--0be8d809bcdf4eeab10d4fd8cbc69a8a 54bb36e1ffb64a389f7961b11932d2ed RX(theta₂₆) 0be8d809bcdf4eeab10d4fd8cbc69a8a--54bb36e1ffb64a389f7961b11932d2ed f4619bd507014ebda2c5f50eb5044ea1 54bb36e1ffb64a389f7961b11932d2ed--f4619bd507014ebda2c5f50eb5044ea1 0b6a3a0e3a6649a9b0bb63bfe24d267e X f4619bd507014ebda2c5f50eb5044ea1--0b6a3a0e3a6649a9b0bb63bfe24d267e 0b6a3a0e3a6649a9b0bb63bfe24d267e--b70722d81219424c856a8de344a1484b 0b6a3a0e3a6649a9b0bb63bfe24d267e--4ca1a54532894600993f431874452973

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-12T10:18:34.692717 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-12T10:18:41.757173 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

References