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_2d2ad8a6a3994530bf627056c9f6e5c2 HEA cluster_bb7f3099fdc84958bb78a16d34f348e1 Tower Chebyshev FM db1d40661d3d45cebc67fce41a298e32 0 a3806818e583408faeea0ff1f39df5b6 RX(1.0*acos(x)) db1d40661d3d45cebc67fce41a298e32--a3806818e583408faeea0ff1f39df5b6 607bc8cf11404b0b93fb962541bddce1 1 896d897ad6f949d8ac13ae5988553031 RX(theta₀) a3806818e583408faeea0ff1f39df5b6--896d897ad6f949d8ac13ae5988553031 82f4471794a4402cb778359ecbe84ec0 RY(theta₃) 896d897ad6f949d8ac13ae5988553031--82f4471794a4402cb778359ecbe84ec0 fd35df36fd1d4eeb93c480788616f19a RX(theta₆) 82f4471794a4402cb778359ecbe84ec0--fd35df36fd1d4eeb93c480788616f19a 3e10317610094ef1b8847b5ba93cde10 fd35df36fd1d4eeb93c480788616f19a--3e10317610094ef1b8847b5ba93cde10 879ebd78df5445098e092b799fb65aae 3e10317610094ef1b8847b5ba93cde10--879ebd78df5445098e092b799fb65aae 3219b015d4f04f1ea8cafa3364808916 RX(theta₉) 879ebd78df5445098e092b799fb65aae--3219b015d4f04f1ea8cafa3364808916 d40d2aeb7db647f7bd57559809ed8421 RY(theta₁₂) 3219b015d4f04f1ea8cafa3364808916--d40d2aeb7db647f7bd57559809ed8421 e8cfb804cae94ac0929966185d44383d RX(theta₁₅) d40d2aeb7db647f7bd57559809ed8421--e8cfb804cae94ac0929966185d44383d 710ff05cff5a40d3b2d88ba740436963 e8cfb804cae94ac0929966185d44383d--710ff05cff5a40d3b2d88ba740436963 a38322dfe3ff4755bf8977c66cba0ebc 710ff05cff5a40d3b2d88ba740436963--a38322dfe3ff4755bf8977c66cba0ebc bf9b941956a84f7b8ceeba265aefe354 RX(theta₁₈) a38322dfe3ff4755bf8977c66cba0ebc--bf9b941956a84f7b8ceeba265aefe354 3555a93feea449f2a560e9fe2bc58f37 RY(theta₂₁) bf9b941956a84f7b8ceeba265aefe354--3555a93feea449f2a560e9fe2bc58f37 ee6e4817ca514099ac7c9f7318a2cffd RX(theta₂₄) 3555a93feea449f2a560e9fe2bc58f37--ee6e4817ca514099ac7c9f7318a2cffd 4710cd61687d4b6598ce1b587537c50d ee6e4817ca514099ac7c9f7318a2cffd--4710cd61687d4b6598ce1b587537c50d a4ced20f0cf944be8fad81026e1ffe09 4710cd61687d4b6598ce1b587537c50d--a4ced20f0cf944be8fad81026e1ffe09 0f1922475fb548dd8d64e821dc0ed7d4 a4ced20f0cf944be8fad81026e1ffe09--0f1922475fb548dd8d64e821dc0ed7d4 101ad283f5d24823b9bbfee0c3963a34 f3c97dadc1af4825ad9a81682b09e6ab RX(2.0*acos(x)) 607bc8cf11404b0b93fb962541bddce1--f3c97dadc1af4825ad9a81682b09e6ab d94b3e8f49f7455bb93f99dac533079f 2 c261634635ea426789b1ab6a6d3e3d44 RX(theta₁) f3c97dadc1af4825ad9a81682b09e6ab--c261634635ea426789b1ab6a6d3e3d44 7e46b0d764384024b34972c1b8b8893b RY(theta₄) c261634635ea426789b1ab6a6d3e3d44--7e46b0d764384024b34972c1b8b8893b 38f716f44c954d4f9de7a39818b4a393 RX(theta₇) 7e46b0d764384024b34972c1b8b8893b--38f716f44c954d4f9de7a39818b4a393 f7bf7bd4293d4d4fa6bd29030777d397 X 38f716f44c954d4f9de7a39818b4a393--f7bf7bd4293d4d4fa6bd29030777d397 f7bf7bd4293d4d4fa6bd29030777d397--3e10317610094ef1b8847b5ba93cde10 7c04512260db4b929cdb7af5c0225bbd f7bf7bd4293d4d4fa6bd29030777d397--7c04512260db4b929cdb7af5c0225bbd d9b0ea04c94f4533bd1eec321a45ce23 RX(theta₁₀) 7c04512260db4b929cdb7af5c0225bbd--d9b0ea04c94f4533bd1eec321a45ce23 64cfaaae97b4414d959bf89c7046c059 RY(theta₁₃) d9b0ea04c94f4533bd1eec321a45ce23--64cfaaae97b4414d959bf89c7046c059 c5de0867d1cd46d78b84b8aaa9d44cd9 RX(theta₁₆) 64cfaaae97b4414d959bf89c7046c059--c5de0867d1cd46d78b84b8aaa9d44cd9 c103209468aa42aea154f01ffcd97663 X c5de0867d1cd46d78b84b8aaa9d44cd9--c103209468aa42aea154f01ffcd97663 c103209468aa42aea154f01ffcd97663--710ff05cff5a40d3b2d88ba740436963 7779202058d3432eb1fc5c77401e5045 c103209468aa42aea154f01ffcd97663--7779202058d3432eb1fc5c77401e5045 e7553d7a3ec640a4bdc8afcae63265a3 RX(theta₁₉) 7779202058d3432eb1fc5c77401e5045--e7553d7a3ec640a4bdc8afcae63265a3 fea4795ad94744d7a4adba7dee45cb63 RY(theta₂₂) e7553d7a3ec640a4bdc8afcae63265a3--fea4795ad94744d7a4adba7dee45cb63 3c0147b99f914f998c7fd8a2bf04502f RX(theta₂₅) fea4795ad94744d7a4adba7dee45cb63--3c0147b99f914f998c7fd8a2bf04502f 1f4dea7270b243eca8339ba1affd57b1 X 3c0147b99f914f998c7fd8a2bf04502f--1f4dea7270b243eca8339ba1affd57b1 1f4dea7270b243eca8339ba1affd57b1--4710cd61687d4b6598ce1b587537c50d c1588d9a04ac445aa7ecf32100a699cb 1f4dea7270b243eca8339ba1affd57b1--c1588d9a04ac445aa7ecf32100a699cb c1588d9a04ac445aa7ecf32100a699cb--101ad283f5d24823b9bbfee0c3963a34 4d27542960f34a9bb942fdd0398a83aa 49236e4c54da4be1935c2276e4f4af7d RX(3.0*acos(x)) d94b3e8f49f7455bb93f99dac533079f--49236e4c54da4be1935c2276e4f4af7d 0d2af2af54774e098a221e6a47b099a2 RX(theta₂) 49236e4c54da4be1935c2276e4f4af7d--0d2af2af54774e098a221e6a47b099a2 ea649de590cf4fb7a3a6a321ff3a77ed RY(theta₅) 0d2af2af54774e098a221e6a47b099a2--ea649de590cf4fb7a3a6a321ff3a77ed 96d9c9035edd49868ed01316b524c1f7 RX(theta₈) ea649de590cf4fb7a3a6a321ff3a77ed--96d9c9035edd49868ed01316b524c1f7 1e13e581ca3e4154909f2a5dc6f9b69d 96d9c9035edd49868ed01316b524c1f7--1e13e581ca3e4154909f2a5dc6f9b69d d420c0aa2c3d46f8b06d92ab72b7533c X 1e13e581ca3e4154909f2a5dc6f9b69d--d420c0aa2c3d46f8b06d92ab72b7533c d420c0aa2c3d46f8b06d92ab72b7533c--7c04512260db4b929cdb7af5c0225bbd 15b88d102a0643c9b52e4ea00a6972b7 RX(theta₁₁) d420c0aa2c3d46f8b06d92ab72b7533c--15b88d102a0643c9b52e4ea00a6972b7 39aec47e5a764421a912576cce721d2e RY(theta₁₄) 15b88d102a0643c9b52e4ea00a6972b7--39aec47e5a764421a912576cce721d2e 7f2411971d644c038df3fce166a7fad2 RX(theta₁₇) 39aec47e5a764421a912576cce721d2e--7f2411971d644c038df3fce166a7fad2 1c365bedc9494c6d8127d4672fbcbd24 7f2411971d644c038df3fce166a7fad2--1c365bedc9494c6d8127d4672fbcbd24 9026ae94e7a245a6b3dbd93f974b022e X 1c365bedc9494c6d8127d4672fbcbd24--9026ae94e7a245a6b3dbd93f974b022e 9026ae94e7a245a6b3dbd93f974b022e--7779202058d3432eb1fc5c77401e5045 65b5076d3be3460c96279a413b500197 RX(theta₂₀) 9026ae94e7a245a6b3dbd93f974b022e--65b5076d3be3460c96279a413b500197 f136eb8e3f794741918430cf1138f403 RY(theta₂₃) 65b5076d3be3460c96279a413b500197--f136eb8e3f794741918430cf1138f403 c5459d8d180d41b8b6d9b870c6bed6ec RX(theta₂₆) f136eb8e3f794741918430cf1138f403--c5459d8d180d41b8b6d9b870c6bed6ec b50aa43bd2024164a0062dc89628ddc7 c5459d8d180d41b8b6d9b870c6bed6ec--b50aa43bd2024164a0062dc89628ddc7 432c2d1daf46422e80a207de7a3576d8 X b50aa43bd2024164a0062dc89628ddc7--432c2d1daf46422e80a207de7a3576d8 432c2d1daf46422e80a207de7a3576d8--c1588d9a04ac445aa7ecf32100a699cb 432c2d1daf46422e80a207de7a3576d8--4d27542960f34a9bb942fdd0398a83aa

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-06-04T14:16:42.019032 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-06-04T14:16:49.519659 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References