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_aa4391ab49664ec8a2b20f9c259a8d2b HEA cluster_65ab37f9b6c149768aefbed44201e365 Tower Chebyshev FM 54327e1c3aee42be9ae99ff376a4224b 0 97f0806149dc42f8836d904d9dea4743 RX(1.0*acos(x)) 54327e1c3aee42be9ae99ff376a4224b--97f0806149dc42f8836d904d9dea4743 a5b203f27e8446d08c4c11e185174e1a 1 7ed2347e180d4036a59cad50463edde0 RX(theta₀) 97f0806149dc42f8836d904d9dea4743--7ed2347e180d4036a59cad50463edde0 7f405cb6e4f84a4ca6b0f98e0685dd91 RY(theta₃) 7ed2347e180d4036a59cad50463edde0--7f405cb6e4f84a4ca6b0f98e0685dd91 7c3592bf098d4a1795939e64b82dc5dc RX(theta₆) 7f405cb6e4f84a4ca6b0f98e0685dd91--7c3592bf098d4a1795939e64b82dc5dc a80271816a2f49819abd54c55e6009cb 7c3592bf098d4a1795939e64b82dc5dc--a80271816a2f49819abd54c55e6009cb 961d96f5ca5c4f24849b0797ec8ad769 a80271816a2f49819abd54c55e6009cb--961d96f5ca5c4f24849b0797ec8ad769 a8cc659ef5404e5e9b2b643e8ce075d9 RX(theta₉) 961d96f5ca5c4f24849b0797ec8ad769--a8cc659ef5404e5e9b2b643e8ce075d9 7020f11a8c8f474c922b783c6808d178 RY(theta₁₂) a8cc659ef5404e5e9b2b643e8ce075d9--7020f11a8c8f474c922b783c6808d178 78344131aaf749f7b83ad6fcbcdd8d0a RX(theta₁₅) 7020f11a8c8f474c922b783c6808d178--78344131aaf749f7b83ad6fcbcdd8d0a 6ee690e1aee44107bede64df4e6264f7 78344131aaf749f7b83ad6fcbcdd8d0a--6ee690e1aee44107bede64df4e6264f7 6bc124ca802c4946bc4778a475198835 6ee690e1aee44107bede64df4e6264f7--6bc124ca802c4946bc4778a475198835 0cfafd21b68b4bddab316b3a246e0fba RX(theta₁₈) 6bc124ca802c4946bc4778a475198835--0cfafd21b68b4bddab316b3a246e0fba 910203ddf99f4baca2800fd6a475c865 RY(theta₂₁) 0cfafd21b68b4bddab316b3a246e0fba--910203ddf99f4baca2800fd6a475c865 e1d7b457381d48eebe7642023a3bccdd RX(theta₂₄) 910203ddf99f4baca2800fd6a475c865--e1d7b457381d48eebe7642023a3bccdd a5824f43dde7430582fefa45d00f5285 e1d7b457381d48eebe7642023a3bccdd--a5824f43dde7430582fefa45d00f5285 a5f84edcf3dd47c88947c94852aa6e36 a5824f43dde7430582fefa45d00f5285--a5f84edcf3dd47c88947c94852aa6e36 87a0f22f29cd46659b55c43701b94560 a5f84edcf3dd47c88947c94852aa6e36--87a0f22f29cd46659b55c43701b94560 036ad712810c452ebb311547b5369527 8c4d9ec0e8bf4198b933a9789f27ddf6 RX(2.0*acos(x)) a5b203f27e8446d08c4c11e185174e1a--8c4d9ec0e8bf4198b933a9789f27ddf6 73d0ad19d19347fb9199ccf88cd091d2 2 3e66a7719dc14d238beecca2ad4ab0dd RX(theta₁) 8c4d9ec0e8bf4198b933a9789f27ddf6--3e66a7719dc14d238beecca2ad4ab0dd efc6dee40df94e74a3858ed3b30cd27f RY(theta₄) 3e66a7719dc14d238beecca2ad4ab0dd--efc6dee40df94e74a3858ed3b30cd27f 19ef5d6b181e413984a7f65bb6137f4d RX(theta₇) efc6dee40df94e74a3858ed3b30cd27f--19ef5d6b181e413984a7f65bb6137f4d 7288220ca86f45e59bdd301cfd68d887 X 19ef5d6b181e413984a7f65bb6137f4d--7288220ca86f45e59bdd301cfd68d887 7288220ca86f45e59bdd301cfd68d887--a80271816a2f49819abd54c55e6009cb 50e914dfb47641209d63b99e0410e37a 7288220ca86f45e59bdd301cfd68d887--50e914dfb47641209d63b99e0410e37a 544c48c14455407f89078f5dfa80e42c RX(theta₁₀) 50e914dfb47641209d63b99e0410e37a--544c48c14455407f89078f5dfa80e42c d7d6519643e74b179c59978fa381ed12 RY(theta₁₃) 544c48c14455407f89078f5dfa80e42c--d7d6519643e74b179c59978fa381ed12 f70b6316e0054e5aa4ae040b23175e90 RX(theta₁₆) d7d6519643e74b179c59978fa381ed12--f70b6316e0054e5aa4ae040b23175e90 702fc7f1695347a4ac510aa43e3978ab X f70b6316e0054e5aa4ae040b23175e90--702fc7f1695347a4ac510aa43e3978ab 702fc7f1695347a4ac510aa43e3978ab--6ee690e1aee44107bede64df4e6264f7 78c4f52ce5f64016bd0a45e4d44331e1 702fc7f1695347a4ac510aa43e3978ab--78c4f52ce5f64016bd0a45e4d44331e1 d7b405b1832c46fd9b5014cb392e397f RX(theta₁₉) 78c4f52ce5f64016bd0a45e4d44331e1--d7b405b1832c46fd9b5014cb392e397f 3afdd6319a5c4bc1a1e64a527485057d RY(theta₂₂) d7b405b1832c46fd9b5014cb392e397f--3afdd6319a5c4bc1a1e64a527485057d 6ccd29e7f4f04575bc0eb34991759cf2 RX(theta₂₅) 3afdd6319a5c4bc1a1e64a527485057d--6ccd29e7f4f04575bc0eb34991759cf2 7938451810d649fbb9a767a6a52c3f11 X 6ccd29e7f4f04575bc0eb34991759cf2--7938451810d649fbb9a767a6a52c3f11 7938451810d649fbb9a767a6a52c3f11--a5824f43dde7430582fefa45d00f5285 df99d4aa06a649a29d5319b65b1fa55a 7938451810d649fbb9a767a6a52c3f11--df99d4aa06a649a29d5319b65b1fa55a df99d4aa06a649a29d5319b65b1fa55a--036ad712810c452ebb311547b5369527 4d4c42027bc549079477117f2354a092 e7a81bad91544585a95742d66013b66f RX(3.0*acos(x)) 73d0ad19d19347fb9199ccf88cd091d2--e7a81bad91544585a95742d66013b66f 52e73e9c658646f69dbe496c8ea931d0 RX(theta₂) e7a81bad91544585a95742d66013b66f--52e73e9c658646f69dbe496c8ea931d0 30873f975ceb4e39b7494b2cba330e94 RY(theta₅) 52e73e9c658646f69dbe496c8ea931d0--30873f975ceb4e39b7494b2cba330e94 3f845e03ae0945cf8fad0f7dc57e84d0 RX(theta₈) 30873f975ceb4e39b7494b2cba330e94--3f845e03ae0945cf8fad0f7dc57e84d0 60a8342159024d7a87f18e9adeb56176 3f845e03ae0945cf8fad0f7dc57e84d0--60a8342159024d7a87f18e9adeb56176 c07f3e812bca41b2a1aee722daee6ea7 X 60a8342159024d7a87f18e9adeb56176--c07f3e812bca41b2a1aee722daee6ea7 c07f3e812bca41b2a1aee722daee6ea7--50e914dfb47641209d63b99e0410e37a b59b665276544baa911fbc137448fce7 RX(theta₁₁) c07f3e812bca41b2a1aee722daee6ea7--b59b665276544baa911fbc137448fce7 f6f96c785a2f471aaee6ad2be1f70be0 RY(theta₁₄) b59b665276544baa911fbc137448fce7--f6f96c785a2f471aaee6ad2be1f70be0 f0bd3fd756e640f4bc208aac774dfce2 RX(theta₁₇) f6f96c785a2f471aaee6ad2be1f70be0--f0bd3fd756e640f4bc208aac774dfce2 bb5c625eae9046e8b18846fb95afabb3 f0bd3fd756e640f4bc208aac774dfce2--bb5c625eae9046e8b18846fb95afabb3 d099cee7e08e400e8deaae9c7d4a097c X bb5c625eae9046e8b18846fb95afabb3--d099cee7e08e400e8deaae9c7d4a097c d099cee7e08e400e8deaae9c7d4a097c--78c4f52ce5f64016bd0a45e4d44331e1 9bffe541f1f74f0da5de7de1e0dbb99e RX(theta₂₀) d099cee7e08e400e8deaae9c7d4a097c--9bffe541f1f74f0da5de7de1e0dbb99e 86811a0fdb374d9e8fdf484477e662cf RY(theta₂₃) 9bffe541f1f74f0da5de7de1e0dbb99e--86811a0fdb374d9e8fdf484477e662cf 202d293e4eb446e18d1f97d67990c0eb RX(theta₂₆) 86811a0fdb374d9e8fdf484477e662cf--202d293e4eb446e18d1f97d67990c0eb 7456d108361f4c7aa6244e908d386321 202d293e4eb446e18d1f97d67990c0eb--7456d108361f4c7aa6244e908d386321 b954547162ba4e73ae2beae1037414d8 X 7456d108361f4c7aa6244e908d386321--b954547162ba4e73ae2beae1037414d8 b954547162ba4e73ae2beae1037414d8--df99d4aa06a649a29d5319b65b1fa55a b954547162ba4e73ae2beae1037414d8--4d4c42027bc549079477117f2354a092

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-09-20T08:35:00.959652 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-09-20T08:35:08.900190 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References