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_c49e89c3ca1b4626b7f9ec6cb906c6c5 HEA cluster_14272b41a20b40bf9b8250434cde8078 Tower Chebyshev FM 4be1b774312549258f782278efad8499 0 f896549f34e3454981fa39d9ae2c5819 RX(1.0*acos(x)) 4be1b774312549258f782278efad8499--f896549f34e3454981fa39d9ae2c5819 7772f7820056480a8d742175e5bf4148 1 0e9985d3e7744ce58900cc41fc64a9ed RX(theta₀) f896549f34e3454981fa39d9ae2c5819--0e9985d3e7744ce58900cc41fc64a9ed 82636c37555b4f09a2307a5a9d0962e0 RY(theta₃) 0e9985d3e7744ce58900cc41fc64a9ed--82636c37555b4f09a2307a5a9d0962e0 974a4476a26b4e2c9519fc76f0223f55 RX(theta₆) 82636c37555b4f09a2307a5a9d0962e0--974a4476a26b4e2c9519fc76f0223f55 745661565fcc4023b0f239307b1603fb 974a4476a26b4e2c9519fc76f0223f55--745661565fcc4023b0f239307b1603fb f617470ef0c04e1db4ce8176e43c4963 745661565fcc4023b0f239307b1603fb--f617470ef0c04e1db4ce8176e43c4963 2ead0c07e5864dfeab4ce92e088e0702 RX(theta₉) f617470ef0c04e1db4ce8176e43c4963--2ead0c07e5864dfeab4ce92e088e0702 de9e384370c74a0d8bb75c4751eab097 RY(theta₁₂) 2ead0c07e5864dfeab4ce92e088e0702--de9e384370c74a0d8bb75c4751eab097 cd52141563094fb48324ff366c83c1f7 RX(theta₁₅) de9e384370c74a0d8bb75c4751eab097--cd52141563094fb48324ff366c83c1f7 f2cde84c7574437fa856a1c6c6bdd960 cd52141563094fb48324ff366c83c1f7--f2cde84c7574437fa856a1c6c6bdd960 2b9df055dfba44a98669fd50171f8c4f f2cde84c7574437fa856a1c6c6bdd960--2b9df055dfba44a98669fd50171f8c4f bb07db6a86ad45a5bf511a9824792867 RX(theta₁₈) 2b9df055dfba44a98669fd50171f8c4f--bb07db6a86ad45a5bf511a9824792867 3ae78aef25b84845b48fbf3da9d54e86 RY(theta₂₁) bb07db6a86ad45a5bf511a9824792867--3ae78aef25b84845b48fbf3da9d54e86 4dbbbe3efb5c4815a771596e4129faf1 RX(theta₂₄) 3ae78aef25b84845b48fbf3da9d54e86--4dbbbe3efb5c4815a771596e4129faf1 69a218ab94b34a509c0ae0831fd3a7e2 4dbbbe3efb5c4815a771596e4129faf1--69a218ab94b34a509c0ae0831fd3a7e2 0aab8b68924e4042b0a42fb015fefb03 69a218ab94b34a509c0ae0831fd3a7e2--0aab8b68924e4042b0a42fb015fefb03 14e70923425c418398213ae87516d0b1 0aab8b68924e4042b0a42fb015fefb03--14e70923425c418398213ae87516d0b1 5ad9d3dc90c546f58fbab6b90eedc260 acdc8bb0fbf242138565daea3a5b2713 RX(2.0*acos(x)) 7772f7820056480a8d742175e5bf4148--acdc8bb0fbf242138565daea3a5b2713 14ff83d4c57640989bbacd1786c07415 2 9e577100d14c441db978d62215d5425f RX(theta₁) acdc8bb0fbf242138565daea3a5b2713--9e577100d14c441db978d62215d5425f fba954a8629840d79a395c7a819969cb RY(theta₄) 9e577100d14c441db978d62215d5425f--fba954a8629840d79a395c7a819969cb 4fd76ed72b314049a759518af931c430 RX(theta₇) fba954a8629840d79a395c7a819969cb--4fd76ed72b314049a759518af931c430 758fcdae0e334e50825793c8100fadf3 X 4fd76ed72b314049a759518af931c430--758fcdae0e334e50825793c8100fadf3 758fcdae0e334e50825793c8100fadf3--745661565fcc4023b0f239307b1603fb 548c49d824764771a2882e0231ede823 758fcdae0e334e50825793c8100fadf3--548c49d824764771a2882e0231ede823 00e0c6799ede44aea2e1a3ba1cd99dde RX(theta₁₀) 548c49d824764771a2882e0231ede823--00e0c6799ede44aea2e1a3ba1cd99dde 0b535180ffad446183fcf4781751c766 RY(theta₁₃) 00e0c6799ede44aea2e1a3ba1cd99dde--0b535180ffad446183fcf4781751c766 171456ed69f74a5c9d37baf77c506a41 RX(theta₁₆) 0b535180ffad446183fcf4781751c766--171456ed69f74a5c9d37baf77c506a41 7791ff2818dc465a9b8fbfdfca374cbd X 171456ed69f74a5c9d37baf77c506a41--7791ff2818dc465a9b8fbfdfca374cbd 7791ff2818dc465a9b8fbfdfca374cbd--f2cde84c7574437fa856a1c6c6bdd960 771cf446a394478aac9f4b58d52157a2 7791ff2818dc465a9b8fbfdfca374cbd--771cf446a394478aac9f4b58d52157a2 2f0591e1206a45798b48dd9a1cd7c0eb RX(theta₁₉) 771cf446a394478aac9f4b58d52157a2--2f0591e1206a45798b48dd9a1cd7c0eb 9fad57ca50614e8e9aabf22da1fa03a0 RY(theta₂₂) 2f0591e1206a45798b48dd9a1cd7c0eb--9fad57ca50614e8e9aabf22da1fa03a0 c7eb7cfe42fe40489e98c6b925cda571 RX(theta₂₅) 9fad57ca50614e8e9aabf22da1fa03a0--c7eb7cfe42fe40489e98c6b925cda571 4af818a125b6463d9ce497f830e7f79e X c7eb7cfe42fe40489e98c6b925cda571--4af818a125b6463d9ce497f830e7f79e 4af818a125b6463d9ce497f830e7f79e--69a218ab94b34a509c0ae0831fd3a7e2 5048ed9ba2bb49f18c779e9b8032ea81 4af818a125b6463d9ce497f830e7f79e--5048ed9ba2bb49f18c779e9b8032ea81 5048ed9ba2bb49f18c779e9b8032ea81--5ad9d3dc90c546f58fbab6b90eedc260 7fed9bb60f7e49d091609f5cb8b53f37 72b79e6bff694c8b89c49fa45c590795 RX(3.0*acos(x)) 14ff83d4c57640989bbacd1786c07415--72b79e6bff694c8b89c49fa45c590795 77d26c35494a4c48894c5b1543ccdfba RX(theta₂) 72b79e6bff694c8b89c49fa45c590795--77d26c35494a4c48894c5b1543ccdfba 2794ec11991347b598aa7e1a4af664c4 RY(theta₅) 77d26c35494a4c48894c5b1543ccdfba--2794ec11991347b598aa7e1a4af664c4 ca89411fa0124de799a3c2076857a3a8 RX(theta₈) 2794ec11991347b598aa7e1a4af664c4--ca89411fa0124de799a3c2076857a3a8 d3a8085e320645c581e902898acd96ca ca89411fa0124de799a3c2076857a3a8--d3a8085e320645c581e902898acd96ca 9bfbf4e5b8174bc58082d369abe4f2fc X d3a8085e320645c581e902898acd96ca--9bfbf4e5b8174bc58082d369abe4f2fc 9bfbf4e5b8174bc58082d369abe4f2fc--548c49d824764771a2882e0231ede823 4e4aba30f12d49edab7d12380f3d1925 RX(theta₁₁) 9bfbf4e5b8174bc58082d369abe4f2fc--4e4aba30f12d49edab7d12380f3d1925 e37510e54c7643d4815f52258d4e68a8 RY(theta₁₄) 4e4aba30f12d49edab7d12380f3d1925--e37510e54c7643d4815f52258d4e68a8 ebe81a2698544680a104a13eed38a4db RX(theta₁₇) e37510e54c7643d4815f52258d4e68a8--ebe81a2698544680a104a13eed38a4db 93fbbb725dec4ed691cf0d2785a0fdf8 ebe81a2698544680a104a13eed38a4db--93fbbb725dec4ed691cf0d2785a0fdf8 5d1fe1e06ea141a788a6fe595a09297d X 93fbbb725dec4ed691cf0d2785a0fdf8--5d1fe1e06ea141a788a6fe595a09297d 5d1fe1e06ea141a788a6fe595a09297d--771cf446a394478aac9f4b58d52157a2 98a480f70bd5476589bfabbd34a38de3 RX(theta₂₀) 5d1fe1e06ea141a788a6fe595a09297d--98a480f70bd5476589bfabbd34a38de3 cd13c8d240c94d6d88af68b0e6eb296a RY(theta₂₃) 98a480f70bd5476589bfabbd34a38de3--cd13c8d240c94d6d88af68b0e6eb296a 4ccdb7dbb1194f0197ddb2a4ba5435c0 RX(theta₂₆) cd13c8d240c94d6d88af68b0e6eb296a--4ccdb7dbb1194f0197ddb2a4ba5435c0 f73afab43b1440ab9a2a0b659a5adeb7 4ccdb7dbb1194f0197ddb2a4ba5435c0--f73afab43b1440ab9a2a0b659a5adeb7 a872a8a1dcd14dd5b183ee0d3ae50b69 X f73afab43b1440ab9a2a0b659a5adeb7--a872a8a1dcd14dd5b183ee0d3ae50b69 a872a8a1dcd14dd5b183ee0d3ae50b69--5048ed9ba2bb49f18c779e9b8032ea81 a872a8a1dcd14dd5b183ee0d3ae50b69--7fed9bb60f7e49d091609f5cb8b53f37

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-01-28T16:51:32.130530 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-01-28T16:51:39.077533 image/svg+xml Matplotlib v3.10.0, https://matplotlib.org/

References