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_f01ea0b1f4da477b8a1f9b0ae6f9b2aa HEA cluster_af1f91e3563f4799aaf52ee8e87120c5 Tower Chebyshev FM ef3514c2771d4279980973349c4ae674 0 9f1b6dd98f58463a90b5884a0736a76a RX(1.0*acos(x)) ef3514c2771d4279980973349c4ae674--9f1b6dd98f58463a90b5884a0736a76a b082998f6bc84f3d8007abf3b354a54c 1 0d825afc3b0c4b9e82c08fedf0025633 RX(theta₀) 9f1b6dd98f58463a90b5884a0736a76a--0d825afc3b0c4b9e82c08fedf0025633 69c075ca25844ba9ad2f22f75d12c447 RY(theta₃) 0d825afc3b0c4b9e82c08fedf0025633--69c075ca25844ba9ad2f22f75d12c447 38c3197125d54e5f8ebb73ab02794892 RX(theta₆) 69c075ca25844ba9ad2f22f75d12c447--38c3197125d54e5f8ebb73ab02794892 7bea6519de494207bc206e01b272f45a 38c3197125d54e5f8ebb73ab02794892--7bea6519de494207bc206e01b272f45a 571e10227fbf426aac2bae242dadfb0e 7bea6519de494207bc206e01b272f45a--571e10227fbf426aac2bae242dadfb0e 47ccc57d58524160b6dedc0a0d121206 RX(theta₉) 571e10227fbf426aac2bae242dadfb0e--47ccc57d58524160b6dedc0a0d121206 df61701fb01e47cf9b227f471f055fbe RY(theta₁₂) 47ccc57d58524160b6dedc0a0d121206--df61701fb01e47cf9b227f471f055fbe a6a8410b872e4e8894858d6a4acc05da RX(theta₁₅) df61701fb01e47cf9b227f471f055fbe--a6a8410b872e4e8894858d6a4acc05da 4237ff8212334f6ba11a9a79bb339bfd a6a8410b872e4e8894858d6a4acc05da--4237ff8212334f6ba11a9a79bb339bfd fcf0e2d3b88e42938737fcb0b0014e61 4237ff8212334f6ba11a9a79bb339bfd--fcf0e2d3b88e42938737fcb0b0014e61 91106c4e6aa24889b96f9252b7df889b RX(theta₁₈) fcf0e2d3b88e42938737fcb0b0014e61--91106c4e6aa24889b96f9252b7df889b 1cc8c352330b42b69fb445defc71f0dd RY(theta₂₁) 91106c4e6aa24889b96f9252b7df889b--1cc8c352330b42b69fb445defc71f0dd a12bc864f80840a98604cc50cf17f5f2 RX(theta₂₄) 1cc8c352330b42b69fb445defc71f0dd--a12bc864f80840a98604cc50cf17f5f2 b11e1029a4b44a8abc03025118cc36d0 a12bc864f80840a98604cc50cf17f5f2--b11e1029a4b44a8abc03025118cc36d0 9350dddff7564b39923d6d5d08885992 b11e1029a4b44a8abc03025118cc36d0--9350dddff7564b39923d6d5d08885992 f7990e62ddbd4395945e737b1c737856 9350dddff7564b39923d6d5d08885992--f7990e62ddbd4395945e737b1c737856 cd3d9b5b659c49f6837908b2e159ca24 a318196d66d5414b849d4d03c870d932 RX(2.0*acos(x)) b082998f6bc84f3d8007abf3b354a54c--a318196d66d5414b849d4d03c870d932 428168f3833b410f803492ebba529a09 2 6015e1d97d2246d6be38eddfcce3f756 RX(theta₁) a318196d66d5414b849d4d03c870d932--6015e1d97d2246d6be38eddfcce3f756 411a28097e7249eeb4ffbdd9accb1cfe RY(theta₄) 6015e1d97d2246d6be38eddfcce3f756--411a28097e7249eeb4ffbdd9accb1cfe 860f121ff0e84ef5888cd6da526d4323 RX(theta₇) 411a28097e7249eeb4ffbdd9accb1cfe--860f121ff0e84ef5888cd6da526d4323 c89ea5c06cad431fa20cf011de543d91 X 860f121ff0e84ef5888cd6da526d4323--c89ea5c06cad431fa20cf011de543d91 c89ea5c06cad431fa20cf011de543d91--7bea6519de494207bc206e01b272f45a b574a2aebd6f416c9436969939a018a6 c89ea5c06cad431fa20cf011de543d91--b574a2aebd6f416c9436969939a018a6 83e98e117f2949ccbff1803130619f64 RX(theta₁₀) b574a2aebd6f416c9436969939a018a6--83e98e117f2949ccbff1803130619f64 ce24b3a15df64ee3b9f50ad8018250ec RY(theta₁₃) 83e98e117f2949ccbff1803130619f64--ce24b3a15df64ee3b9f50ad8018250ec 9cbe2e4420ca4862b353eb0ea36f5218 RX(theta₁₆) ce24b3a15df64ee3b9f50ad8018250ec--9cbe2e4420ca4862b353eb0ea36f5218 613fb5932e9845a5bc29550af78bfafe X 9cbe2e4420ca4862b353eb0ea36f5218--613fb5932e9845a5bc29550af78bfafe 613fb5932e9845a5bc29550af78bfafe--4237ff8212334f6ba11a9a79bb339bfd dd7461ff098f4df1bc6cbfcf23fcaad9 613fb5932e9845a5bc29550af78bfafe--dd7461ff098f4df1bc6cbfcf23fcaad9 7d8afbd751df40a386eca576cea6da57 RX(theta₁₉) dd7461ff098f4df1bc6cbfcf23fcaad9--7d8afbd751df40a386eca576cea6da57 747eb193cf784c06b7e43850e52a9f1a RY(theta₂₂) 7d8afbd751df40a386eca576cea6da57--747eb193cf784c06b7e43850e52a9f1a 8199f9945d1141359b515c9064a7e987 RX(theta₂₅) 747eb193cf784c06b7e43850e52a9f1a--8199f9945d1141359b515c9064a7e987 6b31cc311cfa41b4ad49e8919b15b659 X 8199f9945d1141359b515c9064a7e987--6b31cc311cfa41b4ad49e8919b15b659 6b31cc311cfa41b4ad49e8919b15b659--b11e1029a4b44a8abc03025118cc36d0 a2f06d8d19bf4fcfb8969071e6a1dda1 6b31cc311cfa41b4ad49e8919b15b659--a2f06d8d19bf4fcfb8969071e6a1dda1 a2f06d8d19bf4fcfb8969071e6a1dda1--cd3d9b5b659c49f6837908b2e159ca24 efa2b41f1a6f42968439ee3888775b74 40c9e69cec574633bef32ffd62cffcbb RX(3.0*acos(x)) 428168f3833b410f803492ebba529a09--40c9e69cec574633bef32ffd62cffcbb d73b40fac13848c082c31209b12a00cc RX(theta₂) 40c9e69cec574633bef32ffd62cffcbb--d73b40fac13848c082c31209b12a00cc 9db631fd8c4640759127eff01c581480 RY(theta₅) d73b40fac13848c082c31209b12a00cc--9db631fd8c4640759127eff01c581480 ed5bcebe966c497f8d8f273d14ebe94d RX(theta₈) 9db631fd8c4640759127eff01c581480--ed5bcebe966c497f8d8f273d14ebe94d ca324eb773c54387b2698163a7c55b65 ed5bcebe966c497f8d8f273d14ebe94d--ca324eb773c54387b2698163a7c55b65 2f1a292907bb4a33991ae107d85a875b X ca324eb773c54387b2698163a7c55b65--2f1a292907bb4a33991ae107d85a875b 2f1a292907bb4a33991ae107d85a875b--b574a2aebd6f416c9436969939a018a6 bc5e34f5a05348f18f2c625c408569cd RX(theta₁₁) 2f1a292907bb4a33991ae107d85a875b--bc5e34f5a05348f18f2c625c408569cd a301668458fe43aa80efecfc34236892 RY(theta₁₄) bc5e34f5a05348f18f2c625c408569cd--a301668458fe43aa80efecfc34236892 2a9c4c6dfdc0400bb8c21310cdbe0198 RX(theta₁₇) a301668458fe43aa80efecfc34236892--2a9c4c6dfdc0400bb8c21310cdbe0198 4f9ad7ffe349434eb0874a794d99b811 2a9c4c6dfdc0400bb8c21310cdbe0198--4f9ad7ffe349434eb0874a794d99b811 0de9aef215a142749a81f4e1ace1e435 X 4f9ad7ffe349434eb0874a794d99b811--0de9aef215a142749a81f4e1ace1e435 0de9aef215a142749a81f4e1ace1e435--dd7461ff098f4df1bc6cbfcf23fcaad9 66f64cf725214b4c8ce7de220730ba3e RX(theta₂₀) 0de9aef215a142749a81f4e1ace1e435--66f64cf725214b4c8ce7de220730ba3e 86ae1cac031841a1aa91540c9e7aff61 RY(theta₂₃) 66f64cf725214b4c8ce7de220730ba3e--86ae1cac031841a1aa91540c9e7aff61 9a704fdd320949638137d424d5f21af1 RX(theta₂₆) 86ae1cac031841a1aa91540c9e7aff61--9a704fdd320949638137d424d5f21af1 e46febb5f68748a495938f23d5dbf85a 9a704fdd320949638137d424d5f21af1--e46febb5f68748a495938f23d5dbf85a ea968e83a76f4dcf8163f4fe2cf5e914 X e46febb5f68748a495938f23d5dbf85a--ea968e83a76f4dcf8163f4fe2cf5e914 ea968e83a76f4dcf8163f4fe2cf5e914--a2f06d8d19bf4fcfb8969071e6a1dda1 ea968e83a76f4dcf8163f4fe2cf5e914--efa2b41f1a6f42968439ee3888775b74

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-11-05T09:41:37.487696 image/svg+xml Matplotlib v3.9.2, 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-11-05T09:41:45.548164 image/svg+xml Matplotlib v3.9.2, https://matplotlib.org/

References