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_488a3f0f572247849c0df092afde8367 HEA cluster_34271ff7fe854d0cbbf656465e93583f Tower Chebyshev FM c51c98206fea45b5b6318cc655b6cebf 0 1021553a5a4c40efa1c1a85cdf9d567a RX(1.0*acos(x)) c51c98206fea45b5b6318cc655b6cebf--1021553a5a4c40efa1c1a85cdf9d567a 43511b0f07a4466490052bcf07db2a54 1 5057e08e03694af1960f3c6bb87adc4e RX(theta₀) 1021553a5a4c40efa1c1a85cdf9d567a--5057e08e03694af1960f3c6bb87adc4e e32bcbc4afd642aeb7b8bc14fbc5434f RY(theta₃) 5057e08e03694af1960f3c6bb87adc4e--e32bcbc4afd642aeb7b8bc14fbc5434f 172304257c0645c188bce730d60927d9 RX(theta₆) e32bcbc4afd642aeb7b8bc14fbc5434f--172304257c0645c188bce730d60927d9 a727927d82d2462299b010255a27d462 172304257c0645c188bce730d60927d9--a727927d82d2462299b010255a27d462 292a34b01ffb405fbdc5ae4c1602995e a727927d82d2462299b010255a27d462--292a34b01ffb405fbdc5ae4c1602995e 826057e6f2c44c0ab63701496232fdc4 RX(theta₉) 292a34b01ffb405fbdc5ae4c1602995e--826057e6f2c44c0ab63701496232fdc4 4637d12cebc64b519547851e768b9a8e RY(theta₁₂) 826057e6f2c44c0ab63701496232fdc4--4637d12cebc64b519547851e768b9a8e 6bee4f2ba1924383a9db3620bc4213da RX(theta₁₅) 4637d12cebc64b519547851e768b9a8e--6bee4f2ba1924383a9db3620bc4213da ce0d8190c6bb43deb04ca5f263fd337f 6bee4f2ba1924383a9db3620bc4213da--ce0d8190c6bb43deb04ca5f263fd337f 10d0965adda94336a01938d81d390b2d ce0d8190c6bb43deb04ca5f263fd337f--10d0965adda94336a01938d81d390b2d 10c5befcc4d44c5fb3ae1a613de9f18b RX(theta₁₈) 10d0965adda94336a01938d81d390b2d--10c5befcc4d44c5fb3ae1a613de9f18b c38c68a227ed447b87fb83eff139dfb4 RY(theta₂₁) 10c5befcc4d44c5fb3ae1a613de9f18b--c38c68a227ed447b87fb83eff139dfb4 3133dde725cc435383b15e6814584936 RX(theta₂₄) c38c68a227ed447b87fb83eff139dfb4--3133dde725cc435383b15e6814584936 54947cba475f46db8d34dfbfaee65821 3133dde725cc435383b15e6814584936--54947cba475f46db8d34dfbfaee65821 043705b93d3e44ed9e3fe4aca4889f8f 54947cba475f46db8d34dfbfaee65821--043705b93d3e44ed9e3fe4aca4889f8f ebf7be04b8044fd4a59d4af8867782f3 043705b93d3e44ed9e3fe4aca4889f8f--ebf7be04b8044fd4a59d4af8867782f3 18879615161341f1aa225d39be7b2a17 4beefc2cb3e745db83abd09c873c5a69 RX(2.0*acos(x)) 43511b0f07a4466490052bcf07db2a54--4beefc2cb3e745db83abd09c873c5a69 646885dfd507471c8fdecfb9ebd02d6b 2 3432e5ffbe7c42d393d758c82f919aee RX(theta₁) 4beefc2cb3e745db83abd09c873c5a69--3432e5ffbe7c42d393d758c82f919aee 15a14cea15354155862891ee6d828ceb RY(theta₄) 3432e5ffbe7c42d393d758c82f919aee--15a14cea15354155862891ee6d828ceb 8682d36350004ede84cd7e97f47dff1d RX(theta₇) 15a14cea15354155862891ee6d828ceb--8682d36350004ede84cd7e97f47dff1d 6266af7b4602488bbcdb7aa12d11251d X 8682d36350004ede84cd7e97f47dff1d--6266af7b4602488bbcdb7aa12d11251d 6266af7b4602488bbcdb7aa12d11251d--a727927d82d2462299b010255a27d462 a9777bd9a1544ed19be4e6eb40b96d00 6266af7b4602488bbcdb7aa12d11251d--a9777bd9a1544ed19be4e6eb40b96d00 80943806ade245c79a9be536c64366a2 RX(theta₁₀) a9777bd9a1544ed19be4e6eb40b96d00--80943806ade245c79a9be536c64366a2 714fc93c1ac54f2f8367b0e10cefdd87 RY(theta₁₃) 80943806ade245c79a9be536c64366a2--714fc93c1ac54f2f8367b0e10cefdd87 8a5516e929c94c08a476fb6d6ee51f5b RX(theta₁₆) 714fc93c1ac54f2f8367b0e10cefdd87--8a5516e929c94c08a476fb6d6ee51f5b d583bf158fbd40dfaff192dae8c7396c X 8a5516e929c94c08a476fb6d6ee51f5b--d583bf158fbd40dfaff192dae8c7396c d583bf158fbd40dfaff192dae8c7396c--ce0d8190c6bb43deb04ca5f263fd337f 6892dc8c9a6f47e599935e48cb0b644c d583bf158fbd40dfaff192dae8c7396c--6892dc8c9a6f47e599935e48cb0b644c 8e05c7ff4edf45f3aa59cee7a57077e3 RX(theta₁₉) 6892dc8c9a6f47e599935e48cb0b644c--8e05c7ff4edf45f3aa59cee7a57077e3 e5fb0cfc7db243ca9e71b5a056499dda RY(theta₂₂) 8e05c7ff4edf45f3aa59cee7a57077e3--e5fb0cfc7db243ca9e71b5a056499dda 81b91f2741784bf9a8379f5e3c7dcee4 RX(theta₂₅) e5fb0cfc7db243ca9e71b5a056499dda--81b91f2741784bf9a8379f5e3c7dcee4 16469e52c95244d88311ba7d5212de50 X 81b91f2741784bf9a8379f5e3c7dcee4--16469e52c95244d88311ba7d5212de50 16469e52c95244d88311ba7d5212de50--54947cba475f46db8d34dfbfaee65821 0800b2356fea4047a41ea7b969b4b0dd 16469e52c95244d88311ba7d5212de50--0800b2356fea4047a41ea7b969b4b0dd 0800b2356fea4047a41ea7b969b4b0dd--18879615161341f1aa225d39be7b2a17 47fe358a55734c6283a8927515705e2f 8c851f10ebb245eba426dad30de83e97 RX(3.0*acos(x)) 646885dfd507471c8fdecfb9ebd02d6b--8c851f10ebb245eba426dad30de83e97 f6bb0f4613924c34835d88d5dbb9e99c RX(theta₂) 8c851f10ebb245eba426dad30de83e97--f6bb0f4613924c34835d88d5dbb9e99c 987010117400476295899089f3346c55 RY(theta₅) f6bb0f4613924c34835d88d5dbb9e99c--987010117400476295899089f3346c55 69ba6fdf349446e0b830c617c5738b83 RX(theta₈) 987010117400476295899089f3346c55--69ba6fdf349446e0b830c617c5738b83 35f16474e6554a209e715f3713d57d74 69ba6fdf349446e0b830c617c5738b83--35f16474e6554a209e715f3713d57d74 ffcd8020726449eda96aa13255ce2586 X 35f16474e6554a209e715f3713d57d74--ffcd8020726449eda96aa13255ce2586 ffcd8020726449eda96aa13255ce2586--a9777bd9a1544ed19be4e6eb40b96d00 4544f412d7814f399f5fe09933b976e2 RX(theta₁₁) ffcd8020726449eda96aa13255ce2586--4544f412d7814f399f5fe09933b976e2 54f45bb37c6e453c80769d020cc224b0 RY(theta₁₄) 4544f412d7814f399f5fe09933b976e2--54f45bb37c6e453c80769d020cc224b0 4aac21276e2a4d429deadfbe573eadef RX(theta₁₇) 54f45bb37c6e453c80769d020cc224b0--4aac21276e2a4d429deadfbe573eadef b138394304cc4fb29dce396e90a3be63 4aac21276e2a4d429deadfbe573eadef--b138394304cc4fb29dce396e90a3be63 e1dd13619284431587e98e174999977e X b138394304cc4fb29dce396e90a3be63--e1dd13619284431587e98e174999977e e1dd13619284431587e98e174999977e--6892dc8c9a6f47e599935e48cb0b644c bed658bb247841f0940f584499bd05e8 RX(theta₂₀) e1dd13619284431587e98e174999977e--bed658bb247841f0940f584499bd05e8 48bd80145e83481599175f78b08a1ff2 RY(theta₂₃) bed658bb247841f0940f584499bd05e8--48bd80145e83481599175f78b08a1ff2 c539180ab3e8443389973059835d4e54 RX(theta₂₆) 48bd80145e83481599175f78b08a1ff2--c539180ab3e8443389973059835d4e54 900d6c78930545aea1448c243fd64720 c539180ab3e8443389973059835d4e54--900d6c78930545aea1448c243fd64720 d72cdf8d95714e8ebd8c9ee63233e428 X 900d6c78930545aea1448c243fd64720--d72cdf8d95714e8ebd8c9ee63233e428 d72cdf8d95714e8ebd8c9ee63233e428--0800b2356fea4047a41ea7b969b4b0dd d72cdf8d95714e8ebd8c9ee63233e428--47fe358a55734c6283a8927515705e2f

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-03-13T17:14:30.739044 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-03-13T17:14:38.271701 image/svg+xml Matplotlib v3.10.1, https://matplotlib.org/

References