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_ce98b47c72e14daf90d78b2de2bc9aaa HEA cluster_f60c0014046a4d5c89c324c415ed392c Tower Chebyshev FM c8bac5b50b2e408994969a1ddbf39002 0 f7900543523b40eaa4ff34da33e278ac RX(1.0*acos(x)) c8bac5b50b2e408994969a1ddbf39002--f7900543523b40eaa4ff34da33e278ac 2a84b602e4bf4b7cbc93d6596d8ea9f1 1 418fcfb5e14640918075e78d3a83e914 RX(theta₀) f7900543523b40eaa4ff34da33e278ac--418fcfb5e14640918075e78d3a83e914 b7cb6b23abc6472985d3ead06adab1dd RY(theta₃) 418fcfb5e14640918075e78d3a83e914--b7cb6b23abc6472985d3ead06adab1dd 4593ca3c01df4653bdbc204190aafaca RX(theta₆) b7cb6b23abc6472985d3ead06adab1dd--4593ca3c01df4653bdbc204190aafaca f4c3fee0c9fa441ca209d423293bb809 4593ca3c01df4653bdbc204190aafaca--f4c3fee0c9fa441ca209d423293bb809 3e4099afe6924f93ad6a74b871e5465c f4c3fee0c9fa441ca209d423293bb809--3e4099afe6924f93ad6a74b871e5465c 1ee7f0e776c9470f975687e7db8ec7aa RX(theta₉) 3e4099afe6924f93ad6a74b871e5465c--1ee7f0e776c9470f975687e7db8ec7aa ed028b53d6c040e8a3bb9953df0a3e6d RY(theta₁₂) 1ee7f0e776c9470f975687e7db8ec7aa--ed028b53d6c040e8a3bb9953df0a3e6d 5981a1cad78a4ec2bcbfdbd0ef9b31ed RX(theta₁₅) ed028b53d6c040e8a3bb9953df0a3e6d--5981a1cad78a4ec2bcbfdbd0ef9b31ed 604d8f8b440f4b4caf98a4d9ac9f12b2 5981a1cad78a4ec2bcbfdbd0ef9b31ed--604d8f8b440f4b4caf98a4d9ac9f12b2 e1370b3796b54be7a78f19872c6e9e38 604d8f8b440f4b4caf98a4d9ac9f12b2--e1370b3796b54be7a78f19872c6e9e38 d8d348b9807548e3863fb56bb949b8fe RX(theta₁₈) e1370b3796b54be7a78f19872c6e9e38--d8d348b9807548e3863fb56bb949b8fe 7ae7a61007ce49048958b2c39d13c45d RY(theta₂₁) d8d348b9807548e3863fb56bb949b8fe--7ae7a61007ce49048958b2c39d13c45d 0ae8a4715a9d48db84498da112aadd4e RX(theta₂₄) 7ae7a61007ce49048958b2c39d13c45d--0ae8a4715a9d48db84498da112aadd4e af237dc0b9a94b93ae6f61c14afb307a 0ae8a4715a9d48db84498da112aadd4e--af237dc0b9a94b93ae6f61c14afb307a 567160e61d514c698d0a5d534cddf0d5 af237dc0b9a94b93ae6f61c14afb307a--567160e61d514c698d0a5d534cddf0d5 578442a657474eaaa5ec57e18f74adec 567160e61d514c698d0a5d534cddf0d5--578442a657474eaaa5ec57e18f74adec 598f0da5882942b9bbc512786c111c5e 530bd396c7e74e728b7fc90ec66cdee5 RX(2.0*acos(x)) 2a84b602e4bf4b7cbc93d6596d8ea9f1--530bd396c7e74e728b7fc90ec66cdee5 3aa4c4690d5d42a49246719b6e3cf501 2 a0a2f75382e6468798ab1149510490c2 RX(theta₁) 530bd396c7e74e728b7fc90ec66cdee5--a0a2f75382e6468798ab1149510490c2 b4974e8fe4334c65861871d867edaa9c RY(theta₄) a0a2f75382e6468798ab1149510490c2--b4974e8fe4334c65861871d867edaa9c 8e0b3bbec7d8494c8a16ba4d798d113e RX(theta₇) b4974e8fe4334c65861871d867edaa9c--8e0b3bbec7d8494c8a16ba4d798d113e 7304d1c77dbd424287be39c64e95c411 X 8e0b3bbec7d8494c8a16ba4d798d113e--7304d1c77dbd424287be39c64e95c411 7304d1c77dbd424287be39c64e95c411--f4c3fee0c9fa441ca209d423293bb809 375367ba94be4ebeacf31881b2c5010a 7304d1c77dbd424287be39c64e95c411--375367ba94be4ebeacf31881b2c5010a 031ce4474a154f48b5b5c151d1d0ce3f RX(theta₁₀) 375367ba94be4ebeacf31881b2c5010a--031ce4474a154f48b5b5c151d1d0ce3f 5dbd743dca794e9f901069ee2ce5e88f RY(theta₁₃) 031ce4474a154f48b5b5c151d1d0ce3f--5dbd743dca794e9f901069ee2ce5e88f e5f26c1719e14dad949c6a6ff80e3b1b RX(theta₁₆) 5dbd743dca794e9f901069ee2ce5e88f--e5f26c1719e14dad949c6a6ff80e3b1b 7c754ae87d7c4b3fba03a03bd65a54d8 X e5f26c1719e14dad949c6a6ff80e3b1b--7c754ae87d7c4b3fba03a03bd65a54d8 7c754ae87d7c4b3fba03a03bd65a54d8--604d8f8b440f4b4caf98a4d9ac9f12b2 57167c8d44c745a7a2a1a6ba27e4ca6f 7c754ae87d7c4b3fba03a03bd65a54d8--57167c8d44c745a7a2a1a6ba27e4ca6f e2fa0b1a8a904f4a9d686bc9c7e29e51 RX(theta₁₉) 57167c8d44c745a7a2a1a6ba27e4ca6f--e2fa0b1a8a904f4a9d686bc9c7e29e51 ec8eedf4c3b04ded8c303aa1e52c32db RY(theta₂₂) e2fa0b1a8a904f4a9d686bc9c7e29e51--ec8eedf4c3b04ded8c303aa1e52c32db 1e8c852bf69444ca92484473a9a9772a RX(theta₂₅) ec8eedf4c3b04ded8c303aa1e52c32db--1e8c852bf69444ca92484473a9a9772a cadc9532b7de406ba500b3c0ee5c58d0 X 1e8c852bf69444ca92484473a9a9772a--cadc9532b7de406ba500b3c0ee5c58d0 cadc9532b7de406ba500b3c0ee5c58d0--af237dc0b9a94b93ae6f61c14afb307a d448746f56094225b2db58b69e6ffc76 cadc9532b7de406ba500b3c0ee5c58d0--d448746f56094225b2db58b69e6ffc76 d448746f56094225b2db58b69e6ffc76--598f0da5882942b9bbc512786c111c5e d89386ad28c642b3836665d261729102 0f4b08fe04054dc9a235f264d4885c3f RX(3.0*acos(x)) 3aa4c4690d5d42a49246719b6e3cf501--0f4b08fe04054dc9a235f264d4885c3f 8906416b7b564614bf67987039e06b1b RX(theta₂) 0f4b08fe04054dc9a235f264d4885c3f--8906416b7b564614bf67987039e06b1b 1a8242bf6f0c41608e4f04742cdeb178 RY(theta₅) 8906416b7b564614bf67987039e06b1b--1a8242bf6f0c41608e4f04742cdeb178 d71dd0f9cb854b4aa4c0eaedd8208848 RX(theta₈) 1a8242bf6f0c41608e4f04742cdeb178--d71dd0f9cb854b4aa4c0eaedd8208848 44c801042bf04dd6aa99e6a1aabce278 d71dd0f9cb854b4aa4c0eaedd8208848--44c801042bf04dd6aa99e6a1aabce278 cb747a7c058a4361998d859ff838760a X 44c801042bf04dd6aa99e6a1aabce278--cb747a7c058a4361998d859ff838760a cb747a7c058a4361998d859ff838760a--375367ba94be4ebeacf31881b2c5010a 1eaf9778cef9404e86703565f42d9cc3 RX(theta₁₁) cb747a7c058a4361998d859ff838760a--1eaf9778cef9404e86703565f42d9cc3 2c14c13e81364a9f9f4cb51e7282aa10 RY(theta₁₄) 1eaf9778cef9404e86703565f42d9cc3--2c14c13e81364a9f9f4cb51e7282aa10 5740586d100f4ec49349d1f99596e6ba RX(theta₁₇) 2c14c13e81364a9f9f4cb51e7282aa10--5740586d100f4ec49349d1f99596e6ba 342bda19b4f94d7daa5bceb6779d5f35 5740586d100f4ec49349d1f99596e6ba--342bda19b4f94d7daa5bceb6779d5f35 a08dfa649eab47a49052240aa32af497 X 342bda19b4f94d7daa5bceb6779d5f35--a08dfa649eab47a49052240aa32af497 a08dfa649eab47a49052240aa32af497--57167c8d44c745a7a2a1a6ba27e4ca6f 1af81c40f1f9493e86095312cf7e2b0e RX(theta₂₀) a08dfa649eab47a49052240aa32af497--1af81c40f1f9493e86095312cf7e2b0e 5a644cebac6a4c7d84cd9cafe250a0cc RY(theta₂₃) 1af81c40f1f9493e86095312cf7e2b0e--5a644cebac6a4c7d84cd9cafe250a0cc 416d6c0f28cf47c1830fbecbe05bbc3e RX(theta₂₆) 5a644cebac6a4c7d84cd9cafe250a0cc--416d6c0f28cf47c1830fbecbe05bbc3e 92625d217d604fe18dce8ef9e6e1e510 416d6c0f28cf47c1830fbecbe05bbc3e--92625d217d604fe18dce8ef9e6e1e510 12968140efbe4d32a7e49a95d77ea2e3 X 92625d217d604fe18dce8ef9e6e1e510--12968140efbe4d32a7e49a95d77ea2e3 12968140efbe4d32a7e49a95d77ea2e3--d448746f56094225b2db58b69e6ffc76 12968140efbe4d32a7e49a95d77ea2e3--d89386ad28c642b3836665d261729102

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-06-04T09:58:26.154947 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-06-04T09:58:33.578435 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

References