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_d6d532772809482e86929bce38cf345a HEA cluster_8d4592d58c0e40c09765a62771c25bb7 Tower Chebyshev FM 927d42b6cb3f495da51fac5c13939ea2 0 61f4c2439631460390ac751e2881229f RX(1.0*acos(x)) 927d42b6cb3f495da51fac5c13939ea2--61f4c2439631460390ac751e2881229f 89f520daae684fc6a8a1b315084481b8 1 1c5b1079e18b4688a593871aa3a4a127 RX(theta₀) 61f4c2439631460390ac751e2881229f--1c5b1079e18b4688a593871aa3a4a127 1f002e0b0b1b420e8e7f9daa6bd72288 RY(theta₃) 1c5b1079e18b4688a593871aa3a4a127--1f002e0b0b1b420e8e7f9daa6bd72288 c245e5d6b82641adba2fd3898e742926 RX(theta₆) 1f002e0b0b1b420e8e7f9daa6bd72288--c245e5d6b82641adba2fd3898e742926 d062b518fda246c39814d2cc197efde5 c245e5d6b82641adba2fd3898e742926--d062b518fda246c39814d2cc197efde5 f96642652f1e47b081d99d4c6bbcaded d062b518fda246c39814d2cc197efde5--f96642652f1e47b081d99d4c6bbcaded c941516a419c47eaa7acbcd495e7badc RX(theta₉) f96642652f1e47b081d99d4c6bbcaded--c941516a419c47eaa7acbcd495e7badc 16dfeb586f094096aadf90b45590f57d RY(theta₁₂) c941516a419c47eaa7acbcd495e7badc--16dfeb586f094096aadf90b45590f57d eb5fcc2a5da14ba4b98bfeb70d388141 RX(theta₁₅) 16dfeb586f094096aadf90b45590f57d--eb5fcc2a5da14ba4b98bfeb70d388141 69a62d56bdc641b7bbebd48594362360 eb5fcc2a5da14ba4b98bfeb70d388141--69a62d56bdc641b7bbebd48594362360 9e3ebd6109384b7f90d98e5741430581 69a62d56bdc641b7bbebd48594362360--9e3ebd6109384b7f90d98e5741430581 4f398cc296d24834a08062e79fb951e2 RX(theta₁₈) 9e3ebd6109384b7f90d98e5741430581--4f398cc296d24834a08062e79fb951e2 594a21ecae9547d6b06aed92116dd347 RY(theta₂₁) 4f398cc296d24834a08062e79fb951e2--594a21ecae9547d6b06aed92116dd347 760d45df9855417fa175b8cd43ca572e RX(theta₂₄) 594a21ecae9547d6b06aed92116dd347--760d45df9855417fa175b8cd43ca572e c0147a288e40446ea892e0bb4ab1053b 760d45df9855417fa175b8cd43ca572e--c0147a288e40446ea892e0bb4ab1053b acf06efc678948af94fc52dd8c6c5c9a c0147a288e40446ea892e0bb4ab1053b--acf06efc678948af94fc52dd8c6c5c9a c5888529fbe045e48ed342035d950126 acf06efc678948af94fc52dd8c6c5c9a--c5888529fbe045e48ed342035d950126 8047a80dd2ee4ae1b8cf01ffe3d08615 eed3f3c8f4ac41c29b708bed355d49ac RX(2.0*acos(x)) 89f520daae684fc6a8a1b315084481b8--eed3f3c8f4ac41c29b708bed355d49ac 535f3fe1cc1144ab9f2db24b44efa4cb 2 4cca74a7957a435b8c5a962253f4c56c RX(theta₁) eed3f3c8f4ac41c29b708bed355d49ac--4cca74a7957a435b8c5a962253f4c56c 62e83f3b7e6746ccb0e3b563000b8af1 RY(theta₄) 4cca74a7957a435b8c5a962253f4c56c--62e83f3b7e6746ccb0e3b563000b8af1 fb5198c4979545bd89ae9179900ef9d0 RX(theta₇) 62e83f3b7e6746ccb0e3b563000b8af1--fb5198c4979545bd89ae9179900ef9d0 68444c5c64e7499b969d78c6309a6fda X fb5198c4979545bd89ae9179900ef9d0--68444c5c64e7499b969d78c6309a6fda 68444c5c64e7499b969d78c6309a6fda--d062b518fda246c39814d2cc197efde5 0a7a111efb73431ba91c7fa842d1151f 68444c5c64e7499b969d78c6309a6fda--0a7a111efb73431ba91c7fa842d1151f 56c06c3b535142c69b10ccffb88ce875 RX(theta₁₀) 0a7a111efb73431ba91c7fa842d1151f--56c06c3b535142c69b10ccffb88ce875 62f264fbb9d64d1099c54e644becf5a9 RY(theta₁₃) 56c06c3b535142c69b10ccffb88ce875--62f264fbb9d64d1099c54e644becf5a9 2b7a80dbf04a4104a2ab35b1e2632c5f RX(theta₁₆) 62f264fbb9d64d1099c54e644becf5a9--2b7a80dbf04a4104a2ab35b1e2632c5f bc5d959f343249b4b38cf5cefb260f2a X 2b7a80dbf04a4104a2ab35b1e2632c5f--bc5d959f343249b4b38cf5cefb260f2a bc5d959f343249b4b38cf5cefb260f2a--69a62d56bdc641b7bbebd48594362360 99cf5b5f101e49958f190a1dbb761abc bc5d959f343249b4b38cf5cefb260f2a--99cf5b5f101e49958f190a1dbb761abc 185cef8b99c3463cb8356c717e365061 RX(theta₁₉) 99cf5b5f101e49958f190a1dbb761abc--185cef8b99c3463cb8356c717e365061 83bda6bdc9b64cada7fdbbaa39ea2714 RY(theta₂₂) 185cef8b99c3463cb8356c717e365061--83bda6bdc9b64cada7fdbbaa39ea2714 c13a14e2548944559d504e1b1e55a9ba RX(theta₂₅) 83bda6bdc9b64cada7fdbbaa39ea2714--c13a14e2548944559d504e1b1e55a9ba 11713af40ffb4d4c9a06d6c16503fa6f X c13a14e2548944559d504e1b1e55a9ba--11713af40ffb4d4c9a06d6c16503fa6f 11713af40ffb4d4c9a06d6c16503fa6f--c0147a288e40446ea892e0bb4ab1053b 5cc4526ba2fa47f48268e14748a38e4c 11713af40ffb4d4c9a06d6c16503fa6f--5cc4526ba2fa47f48268e14748a38e4c 5cc4526ba2fa47f48268e14748a38e4c--8047a80dd2ee4ae1b8cf01ffe3d08615 4e57139303f5412182e56af126c4ea77 3f9c3f4ec6a64aa3a99d17533f04d9ae RX(3.0*acos(x)) 535f3fe1cc1144ab9f2db24b44efa4cb--3f9c3f4ec6a64aa3a99d17533f04d9ae d92345822d614152ac972585c3ee78bc RX(theta₂) 3f9c3f4ec6a64aa3a99d17533f04d9ae--d92345822d614152ac972585c3ee78bc a13722f3316c437f8fe30434facb1567 RY(theta₅) d92345822d614152ac972585c3ee78bc--a13722f3316c437f8fe30434facb1567 fa7e4ea6f83f47168e2e7e0a1044b101 RX(theta₈) a13722f3316c437f8fe30434facb1567--fa7e4ea6f83f47168e2e7e0a1044b101 b2d6b45cc3814537a6d5d6c06c8ee151 fa7e4ea6f83f47168e2e7e0a1044b101--b2d6b45cc3814537a6d5d6c06c8ee151 77f58a4cddcd476d961ffa091e8be3cd X b2d6b45cc3814537a6d5d6c06c8ee151--77f58a4cddcd476d961ffa091e8be3cd 77f58a4cddcd476d961ffa091e8be3cd--0a7a111efb73431ba91c7fa842d1151f 33261e3439dc4adfb50927c3d42664f3 RX(theta₁₁) 77f58a4cddcd476d961ffa091e8be3cd--33261e3439dc4adfb50927c3d42664f3 8ead246ca8cd436c97f8774a577f6fc9 RY(theta₁₄) 33261e3439dc4adfb50927c3d42664f3--8ead246ca8cd436c97f8774a577f6fc9 5cb8e13367fa4da2a9a1f09df1b289a6 RX(theta₁₇) 8ead246ca8cd436c97f8774a577f6fc9--5cb8e13367fa4da2a9a1f09df1b289a6 13162531385a469f8793ef6a87ec6094 5cb8e13367fa4da2a9a1f09df1b289a6--13162531385a469f8793ef6a87ec6094 d0b073d329fe4b8b8277f20b1aa29091 X 13162531385a469f8793ef6a87ec6094--d0b073d329fe4b8b8277f20b1aa29091 d0b073d329fe4b8b8277f20b1aa29091--99cf5b5f101e49958f190a1dbb761abc 31b46612ff90448794db5a63a0a116f8 RX(theta₂₀) d0b073d329fe4b8b8277f20b1aa29091--31b46612ff90448794db5a63a0a116f8 6099cdd67e4d46318ebfa936714d94cd RY(theta₂₃) 31b46612ff90448794db5a63a0a116f8--6099cdd67e4d46318ebfa936714d94cd 2e9d26072c5443309558ac5b68edc7ac RX(theta₂₆) 6099cdd67e4d46318ebfa936714d94cd--2e9d26072c5443309558ac5b68edc7ac 7e76245c37144965ace69a2292cb4ff8 2e9d26072c5443309558ac5b68edc7ac--7e76245c37144965ace69a2292cb4ff8 a4c60241c59f41339b6172b743e1d4de X 7e76245c37144965ace69a2292cb4ff8--a4c60241c59f41339b6172b743e1d4de a4c60241c59f41339b6172b743e1d4de--5cc4526ba2fa47f48268e14748a38e4c a4c60241c59f41339b6172b743e1d4de--4e57139303f5412182e56af126c4ea77

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-03T10:07:38.300254 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-03T10:07:46.457251 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References