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_f47dc6c597b14e75bb084d0444703ad6 HEA cluster_d145836b9634423b9ab4ae020ddf8fe3 Tower Chebyshev FM f3e62ee718ef4a13bb0d0ec322cb2dd7 0 4f6fc8c2009a4d3692df0e49263ed9be RX(1.0*acos(x)) f3e62ee718ef4a13bb0d0ec322cb2dd7--4f6fc8c2009a4d3692df0e49263ed9be 88c237559cff45fa98a42f26f4b9d6dd 1 2c6e8ca3f8dc450192251203c33d812d RX(theta₀) 4f6fc8c2009a4d3692df0e49263ed9be--2c6e8ca3f8dc450192251203c33d812d 6708616c6db448f48e5c78257fdf4708 RY(theta₃) 2c6e8ca3f8dc450192251203c33d812d--6708616c6db448f48e5c78257fdf4708 3b53a470a2274b12bc263b9ba58e55dd RX(theta₆) 6708616c6db448f48e5c78257fdf4708--3b53a470a2274b12bc263b9ba58e55dd df3810135a26410f842c3b676c00d216 3b53a470a2274b12bc263b9ba58e55dd--df3810135a26410f842c3b676c00d216 f18c6ad0104e41f7b870a22b1f1ae7f7 df3810135a26410f842c3b676c00d216--f18c6ad0104e41f7b870a22b1f1ae7f7 bbbc263926174ccfb568205803d46632 RX(theta₉) f18c6ad0104e41f7b870a22b1f1ae7f7--bbbc263926174ccfb568205803d46632 7f2a409393464e469bcbc5cf1d3a5b88 RY(theta₁₂) bbbc263926174ccfb568205803d46632--7f2a409393464e469bcbc5cf1d3a5b88 a23f18912ce441edb6e34136822eb60d RX(theta₁₅) 7f2a409393464e469bcbc5cf1d3a5b88--a23f18912ce441edb6e34136822eb60d b8c07b5991cd4fc7a35564dfdda59e9d a23f18912ce441edb6e34136822eb60d--b8c07b5991cd4fc7a35564dfdda59e9d ae64420e81c64971b697d7e2ab4df385 b8c07b5991cd4fc7a35564dfdda59e9d--ae64420e81c64971b697d7e2ab4df385 b55afdb82d894e7284736a6c20039906 RX(theta₁₈) ae64420e81c64971b697d7e2ab4df385--b55afdb82d894e7284736a6c20039906 71e1153758054c9395f3823b3aa94500 RY(theta₂₁) b55afdb82d894e7284736a6c20039906--71e1153758054c9395f3823b3aa94500 7ec5785cab814b5c870dec15f2c3b652 RX(theta₂₄) 71e1153758054c9395f3823b3aa94500--7ec5785cab814b5c870dec15f2c3b652 fd3b8808ff314b46938388047378eb1d 7ec5785cab814b5c870dec15f2c3b652--fd3b8808ff314b46938388047378eb1d e40bf3f915c748f6be20e5618cce60b4 fd3b8808ff314b46938388047378eb1d--e40bf3f915c748f6be20e5618cce60b4 4f97de92d1e24e31865ab8171af72636 e40bf3f915c748f6be20e5618cce60b4--4f97de92d1e24e31865ab8171af72636 574d33434dbd4fb3a32a0510ad9d8aa0 046f1f6d2b8144d4a2cbfef038fe6591 RX(2.0*acos(x)) 88c237559cff45fa98a42f26f4b9d6dd--046f1f6d2b8144d4a2cbfef038fe6591 6f676336025a4f85891571b31ae4ea1c 2 27f84d0e9cba4cf69a19f7f11bbd3600 RX(theta₁) 046f1f6d2b8144d4a2cbfef038fe6591--27f84d0e9cba4cf69a19f7f11bbd3600 8e510d1c616146ddb9e53987ebed2abc RY(theta₄) 27f84d0e9cba4cf69a19f7f11bbd3600--8e510d1c616146ddb9e53987ebed2abc 327fc7fb6e134abb966cb0b4032737c3 RX(theta₇) 8e510d1c616146ddb9e53987ebed2abc--327fc7fb6e134abb966cb0b4032737c3 5bf08ecd0c79420a979b2bd6533a5cce X 327fc7fb6e134abb966cb0b4032737c3--5bf08ecd0c79420a979b2bd6533a5cce 5bf08ecd0c79420a979b2bd6533a5cce--df3810135a26410f842c3b676c00d216 8c4d31be5d844483bfd1354c066ccaaa 5bf08ecd0c79420a979b2bd6533a5cce--8c4d31be5d844483bfd1354c066ccaaa 0546677180a64f99972fbfa341728af8 RX(theta₁₀) 8c4d31be5d844483bfd1354c066ccaaa--0546677180a64f99972fbfa341728af8 0519720ce0a64dd1bf3d230ef8cedb5c RY(theta₁₃) 0546677180a64f99972fbfa341728af8--0519720ce0a64dd1bf3d230ef8cedb5c c686cb04ef7c41a7b1ab4cca85bd4c34 RX(theta₁₆) 0519720ce0a64dd1bf3d230ef8cedb5c--c686cb04ef7c41a7b1ab4cca85bd4c34 9df4e9ceee004d87be191c32d9a6566f X c686cb04ef7c41a7b1ab4cca85bd4c34--9df4e9ceee004d87be191c32d9a6566f 9df4e9ceee004d87be191c32d9a6566f--b8c07b5991cd4fc7a35564dfdda59e9d 879e2d0695574694a02f3bcf4d680a75 9df4e9ceee004d87be191c32d9a6566f--879e2d0695574694a02f3bcf4d680a75 685267f428e344a69e03fb1f74e7bf19 RX(theta₁₉) 879e2d0695574694a02f3bcf4d680a75--685267f428e344a69e03fb1f74e7bf19 47c42918953d4d9db37573a92b26daf0 RY(theta₂₂) 685267f428e344a69e03fb1f74e7bf19--47c42918953d4d9db37573a92b26daf0 4d592f87866a4a18a2975a0d7789a4ba RX(theta₂₅) 47c42918953d4d9db37573a92b26daf0--4d592f87866a4a18a2975a0d7789a4ba e03d1aad996f4e08a99830814eb2b683 X 4d592f87866a4a18a2975a0d7789a4ba--e03d1aad996f4e08a99830814eb2b683 e03d1aad996f4e08a99830814eb2b683--fd3b8808ff314b46938388047378eb1d 1adf8f9b765f45c795e15ae7957c0abe e03d1aad996f4e08a99830814eb2b683--1adf8f9b765f45c795e15ae7957c0abe 1adf8f9b765f45c795e15ae7957c0abe--574d33434dbd4fb3a32a0510ad9d8aa0 e0d30d923aec45a281e9c868cadc6610 5b155fa0ee164c9fa63e8abe04011043 RX(3.0*acos(x)) 6f676336025a4f85891571b31ae4ea1c--5b155fa0ee164c9fa63e8abe04011043 058926c5f31841b58554ead477195c4e RX(theta₂) 5b155fa0ee164c9fa63e8abe04011043--058926c5f31841b58554ead477195c4e 706dae41de8c478587fc9a4bbe445d33 RY(theta₅) 058926c5f31841b58554ead477195c4e--706dae41de8c478587fc9a4bbe445d33 d1759fc0f17f4faa8badadb559cb87be RX(theta₈) 706dae41de8c478587fc9a4bbe445d33--d1759fc0f17f4faa8badadb559cb87be 67ea94fb95bc46f699f4365d750f311d d1759fc0f17f4faa8badadb559cb87be--67ea94fb95bc46f699f4365d750f311d d0b9f1108e6441df9b0daf1213293003 X 67ea94fb95bc46f699f4365d750f311d--d0b9f1108e6441df9b0daf1213293003 d0b9f1108e6441df9b0daf1213293003--8c4d31be5d844483bfd1354c066ccaaa 4bd27493458047329900612d5d91dc10 RX(theta₁₁) d0b9f1108e6441df9b0daf1213293003--4bd27493458047329900612d5d91dc10 c47f72fc22c148068a4b24ac913292af RY(theta₁₄) 4bd27493458047329900612d5d91dc10--c47f72fc22c148068a4b24ac913292af 72d1cd6f71b74d13820ec8572313b42c RX(theta₁₇) c47f72fc22c148068a4b24ac913292af--72d1cd6f71b74d13820ec8572313b42c dc6133e4e2464f929c9d617b1a4ca888 72d1cd6f71b74d13820ec8572313b42c--dc6133e4e2464f929c9d617b1a4ca888 47c103a4cb2548d2ac6bd820fc00bdfb X dc6133e4e2464f929c9d617b1a4ca888--47c103a4cb2548d2ac6bd820fc00bdfb 47c103a4cb2548d2ac6bd820fc00bdfb--879e2d0695574694a02f3bcf4d680a75 85af837d49844789b5cfcbe2950feb0d RX(theta₂₀) 47c103a4cb2548d2ac6bd820fc00bdfb--85af837d49844789b5cfcbe2950feb0d e56944d31fd44290a52545c008cadfd1 RY(theta₂₃) 85af837d49844789b5cfcbe2950feb0d--e56944d31fd44290a52545c008cadfd1 bc58c38215de49b486753efb1719cfaa RX(theta₂₆) e56944d31fd44290a52545c008cadfd1--bc58c38215de49b486753efb1719cfaa 688c80c037dd4a8a8df25c6a58406ac9 bc58c38215de49b486753efb1719cfaa--688c80c037dd4a8a8df25c6a58406ac9 25b6359cfbbb476fa2248fe7a14c3f8f X 688c80c037dd4a8a8df25c6a58406ac9--25b6359cfbbb476fa2248fe7a14c3f8f 25b6359cfbbb476fa2248fe7a14c3f8f--1adf8f9b765f45c795e15ae7957c0abe 25b6359cfbbb476fa2248fe7a14c3f8f--e0d30d923aec45a281e9c868cadc6610

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-21T16:34:59.466658 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-21T16:35:07.282083 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References