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_957002019d4f43e08242b51a55660198 HEA cluster_45e02b4bec704f2fac3504c774cb66e3 Tower Chebyshev FM 5630fa5385594dd4ad17e136527142a5 0 bbed6bd40cad478fa941880097912754 RX(1.0*acos(x)) 5630fa5385594dd4ad17e136527142a5--bbed6bd40cad478fa941880097912754 89a23e82bc0049f5af4a114809742be8 1 00d041a2479d4198b6bc76c3f41999f1 RX(theta₀) bbed6bd40cad478fa941880097912754--00d041a2479d4198b6bc76c3f41999f1 3979c53aedd4426597a616488cf21a6b RY(theta₃) 00d041a2479d4198b6bc76c3f41999f1--3979c53aedd4426597a616488cf21a6b a43f9a6ffe954a0eb22e105ac004dd15 RX(theta₆) 3979c53aedd4426597a616488cf21a6b--a43f9a6ffe954a0eb22e105ac004dd15 30e378b66039481986b3119846d4e250 a43f9a6ffe954a0eb22e105ac004dd15--30e378b66039481986b3119846d4e250 175a526ce3364d049a67c16a966b1a28 30e378b66039481986b3119846d4e250--175a526ce3364d049a67c16a966b1a28 88abcb1241dc4e07aebfef59b5cecf3d RX(theta₉) 175a526ce3364d049a67c16a966b1a28--88abcb1241dc4e07aebfef59b5cecf3d ed6e5ff11f634e4ebf82d3e0972583d8 RY(theta₁₂) 88abcb1241dc4e07aebfef59b5cecf3d--ed6e5ff11f634e4ebf82d3e0972583d8 e01d92e0e12d4e2a85fad7f7e429b779 RX(theta₁₅) ed6e5ff11f634e4ebf82d3e0972583d8--e01d92e0e12d4e2a85fad7f7e429b779 b7e6e5129e4943bd9284e9aeec0a073e e01d92e0e12d4e2a85fad7f7e429b779--b7e6e5129e4943bd9284e9aeec0a073e 72682bf81e304b778e3314b2646634bc b7e6e5129e4943bd9284e9aeec0a073e--72682bf81e304b778e3314b2646634bc 54691bbeb70e463eacc3e3580a182a46 RX(theta₁₈) 72682bf81e304b778e3314b2646634bc--54691bbeb70e463eacc3e3580a182a46 8a64b0543e0542dea62749bc4721a78f RY(theta₂₁) 54691bbeb70e463eacc3e3580a182a46--8a64b0543e0542dea62749bc4721a78f 4e5a0190be744b2ab13a4ebc8598c94a RX(theta₂₄) 8a64b0543e0542dea62749bc4721a78f--4e5a0190be744b2ab13a4ebc8598c94a b645679a86d24bb3a281ddcab56bc254 4e5a0190be744b2ab13a4ebc8598c94a--b645679a86d24bb3a281ddcab56bc254 6e1ed53a5fa24e26bbbb01c89451fdfd b645679a86d24bb3a281ddcab56bc254--6e1ed53a5fa24e26bbbb01c89451fdfd 712f89fcb646430f80119d398a0e9d14 6e1ed53a5fa24e26bbbb01c89451fdfd--712f89fcb646430f80119d398a0e9d14 551207e1cfaf4e1cad8a2be1966ee74d f3a7d313a8a040a582547974e2319b7e RX(2.0*acos(x)) 89a23e82bc0049f5af4a114809742be8--f3a7d313a8a040a582547974e2319b7e 6a2a5ec4d6434b8bb13f2384e1369aa8 2 1d19f8cea6874a1e8ea1d046e4476031 RX(theta₁) f3a7d313a8a040a582547974e2319b7e--1d19f8cea6874a1e8ea1d046e4476031 b2f6dd5ee8d74296968f09594e3f2df9 RY(theta₄) 1d19f8cea6874a1e8ea1d046e4476031--b2f6dd5ee8d74296968f09594e3f2df9 70f8378511174b22841a20f9071a5d52 RX(theta₇) b2f6dd5ee8d74296968f09594e3f2df9--70f8378511174b22841a20f9071a5d52 0bb404c2238d46a0b7ce4332c8804a8e X 70f8378511174b22841a20f9071a5d52--0bb404c2238d46a0b7ce4332c8804a8e 0bb404c2238d46a0b7ce4332c8804a8e--30e378b66039481986b3119846d4e250 43da5c084a7f407cbed07928bd7cea69 0bb404c2238d46a0b7ce4332c8804a8e--43da5c084a7f407cbed07928bd7cea69 ff733965cb8d4d53a8795db4bbd76e7d RX(theta₁₀) 43da5c084a7f407cbed07928bd7cea69--ff733965cb8d4d53a8795db4bbd76e7d 88bbb725b558456f8c19a8061d93ba32 RY(theta₁₃) ff733965cb8d4d53a8795db4bbd76e7d--88bbb725b558456f8c19a8061d93ba32 629d7f4f1aef41e38b804a5eb4ec6508 RX(theta₁₆) 88bbb725b558456f8c19a8061d93ba32--629d7f4f1aef41e38b804a5eb4ec6508 ddcf5737af9140979934568155c1341e X 629d7f4f1aef41e38b804a5eb4ec6508--ddcf5737af9140979934568155c1341e ddcf5737af9140979934568155c1341e--b7e6e5129e4943bd9284e9aeec0a073e ca37a7cef00f451cb7e2d1c527c6812a ddcf5737af9140979934568155c1341e--ca37a7cef00f451cb7e2d1c527c6812a 43b3c5e6328042cb8d5b64bf44ddcd8a RX(theta₁₉) ca37a7cef00f451cb7e2d1c527c6812a--43b3c5e6328042cb8d5b64bf44ddcd8a 25f6a5c783654d749b2ec47559f9839c RY(theta₂₂) 43b3c5e6328042cb8d5b64bf44ddcd8a--25f6a5c783654d749b2ec47559f9839c fd2e32059d8645e7a83e7d1109bedb1d RX(theta₂₅) 25f6a5c783654d749b2ec47559f9839c--fd2e32059d8645e7a83e7d1109bedb1d 9a06d368805b46fb93f30724c5cc4721 X fd2e32059d8645e7a83e7d1109bedb1d--9a06d368805b46fb93f30724c5cc4721 9a06d368805b46fb93f30724c5cc4721--b645679a86d24bb3a281ddcab56bc254 ac3dbe5cdbb7452fade5bca0536328d7 9a06d368805b46fb93f30724c5cc4721--ac3dbe5cdbb7452fade5bca0536328d7 ac3dbe5cdbb7452fade5bca0536328d7--551207e1cfaf4e1cad8a2be1966ee74d 5b73ff45dd024a61bc8128b955a93dea c3bce91c43144fc68d4dffaa133fb230 RX(3.0*acos(x)) 6a2a5ec4d6434b8bb13f2384e1369aa8--c3bce91c43144fc68d4dffaa133fb230 d51bd30cfb854a7e828976b0a6a4034e RX(theta₂) c3bce91c43144fc68d4dffaa133fb230--d51bd30cfb854a7e828976b0a6a4034e bf273fdc6c7147d8b5c911f6c7e6b3af RY(theta₅) d51bd30cfb854a7e828976b0a6a4034e--bf273fdc6c7147d8b5c911f6c7e6b3af 2f53a113a8224c2c8ecbbfbd6283f64d RX(theta₈) bf273fdc6c7147d8b5c911f6c7e6b3af--2f53a113a8224c2c8ecbbfbd6283f64d 6b863a24f7e64e62abb957efb799bbe2 2f53a113a8224c2c8ecbbfbd6283f64d--6b863a24f7e64e62abb957efb799bbe2 bb5cf058520145f5b4ec90834b114d62 X 6b863a24f7e64e62abb957efb799bbe2--bb5cf058520145f5b4ec90834b114d62 bb5cf058520145f5b4ec90834b114d62--43da5c084a7f407cbed07928bd7cea69 dda6334db03842788a494cedfcd322fa RX(theta₁₁) bb5cf058520145f5b4ec90834b114d62--dda6334db03842788a494cedfcd322fa 6e640435e5364d6f82040fdb21f85961 RY(theta₁₄) dda6334db03842788a494cedfcd322fa--6e640435e5364d6f82040fdb21f85961 1e329a6d36ab4ce5a7f26e43d463a896 RX(theta₁₇) 6e640435e5364d6f82040fdb21f85961--1e329a6d36ab4ce5a7f26e43d463a896 b51f02c7f6cb40939460911df7b368dc 1e329a6d36ab4ce5a7f26e43d463a896--b51f02c7f6cb40939460911df7b368dc 1716bd7e7b224cb89ba1cd0e2e5ce6db X b51f02c7f6cb40939460911df7b368dc--1716bd7e7b224cb89ba1cd0e2e5ce6db 1716bd7e7b224cb89ba1cd0e2e5ce6db--ca37a7cef00f451cb7e2d1c527c6812a 07ab8d6be4954a7cacdfbbc248db786b RX(theta₂₀) 1716bd7e7b224cb89ba1cd0e2e5ce6db--07ab8d6be4954a7cacdfbbc248db786b 8ef834f16f69463db4aa5c57e93052df RY(theta₂₃) 07ab8d6be4954a7cacdfbbc248db786b--8ef834f16f69463db4aa5c57e93052df 817bfa556fe54598a47d3120597d833d RX(theta₂₆) 8ef834f16f69463db4aa5c57e93052df--817bfa556fe54598a47d3120597d833d d98d64ef8cfb498bb96f37848ee7d2f9 817bfa556fe54598a47d3120597d833d--d98d64ef8cfb498bb96f37848ee7d2f9 935935cc5e1649bb8482cb343b5abd05 X d98d64ef8cfb498bb96f37848ee7d2f9--935935cc5e1649bb8482cb343b5abd05 935935cc5e1649bb8482cb343b5abd05--ac3dbe5cdbb7452fade5bca0536328d7 935935cc5e1649bb8482cb343b5abd05--5b73ff45dd024a61bc8128b955a93dea

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")
2024-08-22T12:51:37.775512 image/svg+xml Matplotlib v3.7.5, 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")
2024-08-22T12:51:45.822559 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References