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_70dee7f3f0994b16a80500785779a844 HEA cluster_afe9d85ba9db49b583c6597b9924ea80 Tower Chebyshev FM b7b740e127d346ecbc3e75387c964f1a 0 09b93ed96ad94315a51d907fdd3cd5b6 RX(1.0*acos(x)) b7b740e127d346ecbc3e75387c964f1a--09b93ed96ad94315a51d907fdd3cd5b6 27fa5c423d7d427f9ac10d6f22046319 1 e0c69c31146443b299e833686948da9e RX(theta₀) 09b93ed96ad94315a51d907fdd3cd5b6--e0c69c31146443b299e833686948da9e e0fc49dfe16a4504add36316f9bf6c46 RY(theta₃) e0c69c31146443b299e833686948da9e--e0fc49dfe16a4504add36316f9bf6c46 03cd83bb708049fcb463158efaab46af RX(theta₆) e0fc49dfe16a4504add36316f9bf6c46--03cd83bb708049fcb463158efaab46af 21f934df6c92498d80a8ed8adfdba9a3 03cd83bb708049fcb463158efaab46af--21f934df6c92498d80a8ed8adfdba9a3 7a3f3862f44248a1b2e330f5407b9bf5 21f934df6c92498d80a8ed8adfdba9a3--7a3f3862f44248a1b2e330f5407b9bf5 6b761f16f1474271bcbbec1d89b573d0 RX(theta₉) 7a3f3862f44248a1b2e330f5407b9bf5--6b761f16f1474271bcbbec1d89b573d0 ee0b2e084b7e4ad380834a251b46a706 RY(theta₁₂) 6b761f16f1474271bcbbec1d89b573d0--ee0b2e084b7e4ad380834a251b46a706 3116d4d22f8f41d98c66a07df3052af2 RX(theta₁₅) ee0b2e084b7e4ad380834a251b46a706--3116d4d22f8f41d98c66a07df3052af2 61e2081fd67d4354a0dbac4b311c31f3 3116d4d22f8f41d98c66a07df3052af2--61e2081fd67d4354a0dbac4b311c31f3 2acb36f7e05546eb905971cbb95af890 61e2081fd67d4354a0dbac4b311c31f3--2acb36f7e05546eb905971cbb95af890 59671b5c851742dba45877993b906d9c RX(theta₁₈) 2acb36f7e05546eb905971cbb95af890--59671b5c851742dba45877993b906d9c fc953ff980274123906de4d8a15f1b66 RY(theta₂₁) 59671b5c851742dba45877993b906d9c--fc953ff980274123906de4d8a15f1b66 c7c3321f163a4e9b97b7b9f2e8076139 RX(theta₂₄) fc953ff980274123906de4d8a15f1b66--c7c3321f163a4e9b97b7b9f2e8076139 5bec3773fd594f4f967487ed967afab5 c7c3321f163a4e9b97b7b9f2e8076139--5bec3773fd594f4f967487ed967afab5 7916805bc1ef4c4982b40602502da8d9 5bec3773fd594f4f967487ed967afab5--7916805bc1ef4c4982b40602502da8d9 d3f33a9dd9ad4c4b80e0075ddc536f3c 7916805bc1ef4c4982b40602502da8d9--d3f33a9dd9ad4c4b80e0075ddc536f3c b68365653a6e41f48e82255085c82f30 1cce319cf5674e25bf4d9bf524742d18 RX(2.0*acos(x)) 27fa5c423d7d427f9ac10d6f22046319--1cce319cf5674e25bf4d9bf524742d18 de0d3b7257cb4143873e5f6508b05beb 2 82e2c87a84004338a7a2e43b8389a25b RX(theta₁) 1cce319cf5674e25bf4d9bf524742d18--82e2c87a84004338a7a2e43b8389a25b c6d9de21cc0d4dc892ebfc2522237260 RY(theta₄) 82e2c87a84004338a7a2e43b8389a25b--c6d9de21cc0d4dc892ebfc2522237260 8eac7a3085234dc7a72285a6dd6d6e49 RX(theta₇) c6d9de21cc0d4dc892ebfc2522237260--8eac7a3085234dc7a72285a6dd6d6e49 95796fa50ff849f7baf560884d593fbc X 8eac7a3085234dc7a72285a6dd6d6e49--95796fa50ff849f7baf560884d593fbc 95796fa50ff849f7baf560884d593fbc--21f934df6c92498d80a8ed8adfdba9a3 989a594a9a2d42e788c0d0de05c11fa7 95796fa50ff849f7baf560884d593fbc--989a594a9a2d42e788c0d0de05c11fa7 0e1233b441444e5ba36bc45c2a3ee645 RX(theta₁₀) 989a594a9a2d42e788c0d0de05c11fa7--0e1233b441444e5ba36bc45c2a3ee645 fd22c0b90b094b9daf709389975939a2 RY(theta₁₃) 0e1233b441444e5ba36bc45c2a3ee645--fd22c0b90b094b9daf709389975939a2 8a0394b4bee540a2ba3bf89282b4a384 RX(theta₁₆) fd22c0b90b094b9daf709389975939a2--8a0394b4bee540a2ba3bf89282b4a384 7e129acc790749beb901ddb7c1acc148 X 8a0394b4bee540a2ba3bf89282b4a384--7e129acc790749beb901ddb7c1acc148 7e129acc790749beb901ddb7c1acc148--61e2081fd67d4354a0dbac4b311c31f3 aed56abaf5824c048e75db49e5434f89 7e129acc790749beb901ddb7c1acc148--aed56abaf5824c048e75db49e5434f89 249ca81873114a808fc72f2f05bf37fb RX(theta₁₉) aed56abaf5824c048e75db49e5434f89--249ca81873114a808fc72f2f05bf37fb a8662da3410e4c849d635c8ccb1f10ab RY(theta₂₂) 249ca81873114a808fc72f2f05bf37fb--a8662da3410e4c849d635c8ccb1f10ab 486a4a280e5a4a8b9195c6f1ac34ec4f RX(theta₂₅) a8662da3410e4c849d635c8ccb1f10ab--486a4a280e5a4a8b9195c6f1ac34ec4f 306626276f204335bc75601ac283bd94 X 486a4a280e5a4a8b9195c6f1ac34ec4f--306626276f204335bc75601ac283bd94 306626276f204335bc75601ac283bd94--5bec3773fd594f4f967487ed967afab5 4d1bbe638b4f48129c09a12cef6c036f 306626276f204335bc75601ac283bd94--4d1bbe638b4f48129c09a12cef6c036f 4d1bbe638b4f48129c09a12cef6c036f--b68365653a6e41f48e82255085c82f30 6e8b688f2ef44a6cac43f971e54a6fa5 c34d4ffb074543a1a309f05f4eadad96 RX(3.0*acos(x)) de0d3b7257cb4143873e5f6508b05beb--c34d4ffb074543a1a309f05f4eadad96 57c860e8a0b5407ba3c9ba4830ebdebc RX(theta₂) c34d4ffb074543a1a309f05f4eadad96--57c860e8a0b5407ba3c9ba4830ebdebc 06de7de50c1748ab9cbc78ac1b6e0d2b RY(theta₅) 57c860e8a0b5407ba3c9ba4830ebdebc--06de7de50c1748ab9cbc78ac1b6e0d2b 7dea361f00f2470da73b46efae08a2a4 RX(theta₈) 06de7de50c1748ab9cbc78ac1b6e0d2b--7dea361f00f2470da73b46efae08a2a4 52034a8474214522bfec78471ecd4324 7dea361f00f2470da73b46efae08a2a4--52034a8474214522bfec78471ecd4324 c3886338061747fb9a211e936bbfa748 X 52034a8474214522bfec78471ecd4324--c3886338061747fb9a211e936bbfa748 c3886338061747fb9a211e936bbfa748--989a594a9a2d42e788c0d0de05c11fa7 8295690d48964c05bb3ce1a874b85e08 RX(theta₁₁) c3886338061747fb9a211e936bbfa748--8295690d48964c05bb3ce1a874b85e08 9f2b6dfb9bc54a7badc31452d2fe248d RY(theta₁₄) 8295690d48964c05bb3ce1a874b85e08--9f2b6dfb9bc54a7badc31452d2fe248d 61f43b4fba764abf861bfd7b409fbf1c RX(theta₁₇) 9f2b6dfb9bc54a7badc31452d2fe248d--61f43b4fba764abf861bfd7b409fbf1c 5ee86653226a43efb08e0bca34432e59 61f43b4fba764abf861bfd7b409fbf1c--5ee86653226a43efb08e0bca34432e59 b6993231e58a4c21a00bcfe62b33ce47 X 5ee86653226a43efb08e0bca34432e59--b6993231e58a4c21a00bcfe62b33ce47 b6993231e58a4c21a00bcfe62b33ce47--aed56abaf5824c048e75db49e5434f89 09d143943a4e412b90aa80bfa24a8931 RX(theta₂₀) b6993231e58a4c21a00bcfe62b33ce47--09d143943a4e412b90aa80bfa24a8931 ce6411fc12a84f66afe3b30ba8b9c3f7 RY(theta₂₃) 09d143943a4e412b90aa80bfa24a8931--ce6411fc12a84f66afe3b30ba8b9c3f7 1d54ff0f37ca44e3aefefdaa86e0f8f3 RX(theta₂₆) ce6411fc12a84f66afe3b30ba8b9c3f7--1d54ff0f37ca44e3aefefdaa86e0f8f3 a3ac91e66fac47c28fbf890716cc1887 1d54ff0f37ca44e3aefefdaa86e0f8f3--a3ac91e66fac47c28fbf890716cc1887 3481d9df31874c7b94ee9e28c65c0484 X a3ac91e66fac47c28fbf890716cc1887--3481d9df31874c7b94ee9e28c65c0484 3481d9df31874c7b94ee9e28c65c0484--4d1bbe638b4f48129c09a12cef6c036f 3481d9df31874c7b94ee9e28c65c0484--6e8b688f2ef44a6cac43f971e54a6fa5

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-07-10T14:30:06.437222 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-07-10T14:30:14.241320 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References