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_474b8253458444a7b0d80851878efce5 HEA cluster_aff673ef9cc0485a84d8ec3950f41e77 Tower Chebyshev FM 364706e01bef41a0b9a8c69aef0cb3f0 0 f969d1185e79409e942d6cdf3a5590bb RX(1.0*acos(x)) 364706e01bef41a0b9a8c69aef0cb3f0--f969d1185e79409e942d6cdf3a5590bb 6eb2953d7b4041acad7cbce65ee33faf 1 4d2688a458414e3b813d3bcfe921bde8 RX(theta₀) f969d1185e79409e942d6cdf3a5590bb--4d2688a458414e3b813d3bcfe921bde8 36a6334054614b909faa21b2f6c2dd9a RY(theta₃) 4d2688a458414e3b813d3bcfe921bde8--36a6334054614b909faa21b2f6c2dd9a 5a7a3bae2909468dab78494fa7a13567 RX(theta₆) 36a6334054614b909faa21b2f6c2dd9a--5a7a3bae2909468dab78494fa7a13567 c44c55e1632543c4a8bff344896dd9dc 5a7a3bae2909468dab78494fa7a13567--c44c55e1632543c4a8bff344896dd9dc 6773726c46e54509821305e1b6329a6e c44c55e1632543c4a8bff344896dd9dc--6773726c46e54509821305e1b6329a6e 6512fd60c1144fee8da6ca56cc6d69d3 RX(theta₉) 6773726c46e54509821305e1b6329a6e--6512fd60c1144fee8da6ca56cc6d69d3 2809cd7ac29c4552a6e66ddaf196de6f RY(theta₁₂) 6512fd60c1144fee8da6ca56cc6d69d3--2809cd7ac29c4552a6e66ddaf196de6f 8042a03d272343bc88a91b6627ea6f23 RX(theta₁₅) 2809cd7ac29c4552a6e66ddaf196de6f--8042a03d272343bc88a91b6627ea6f23 c4dd72a8e23841d8a5104dc500ce8bd1 8042a03d272343bc88a91b6627ea6f23--c4dd72a8e23841d8a5104dc500ce8bd1 61041b7ed7574116bd92447544f0e5dc c4dd72a8e23841d8a5104dc500ce8bd1--61041b7ed7574116bd92447544f0e5dc 55525c34784f400cbafdc9a715f2dec5 RX(theta₁₈) 61041b7ed7574116bd92447544f0e5dc--55525c34784f400cbafdc9a715f2dec5 4110ec7ecae0440a837506394d458b07 RY(theta₂₁) 55525c34784f400cbafdc9a715f2dec5--4110ec7ecae0440a837506394d458b07 0d10e30701bb429c84aa9975387c093e RX(theta₂₄) 4110ec7ecae0440a837506394d458b07--0d10e30701bb429c84aa9975387c093e 21a08bb235e84e39bdb0ff306078fd88 0d10e30701bb429c84aa9975387c093e--21a08bb235e84e39bdb0ff306078fd88 33a82d3f854f4aff8993975701e4d7e2 21a08bb235e84e39bdb0ff306078fd88--33a82d3f854f4aff8993975701e4d7e2 2480f3794283456d823694143b438185 33a82d3f854f4aff8993975701e4d7e2--2480f3794283456d823694143b438185 df6a5243b81c4aa89ec5fe66c84bdfd2 1deb565540fc40219cfa47cf04d3003b RX(2.0*acos(x)) 6eb2953d7b4041acad7cbce65ee33faf--1deb565540fc40219cfa47cf04d3003b f309a989b5ab45f69be79b1330e30ba9 2 d30c731cf4d94d9e908f31d34345d23e RX(theta₁) 1deb565540fc40219cfa47cf04d3003b--d30c731cf4d94d9e908f31d34345d23e 2dd2daef53a74633a6693a421d3d1a48 RY(theta₄) d30c731cf4d94d9e908f31d34345d23e--2dd2daef53a74633a6693a421d3d1a48 809bf9196b724b6e86c2417e08977348 RX(theta₇) 2dd2daef53a74633a6693a421d3d1a48--809bf9196b724b6e86c2417e08977348 4051b9b901c24c12a477033faea7275c X 809bf9196b724b6e86c2417e08977348--4051b9b901c24c12a477033faea7275c 4051b9b901c24c12a477033faea7275c--c44c55e1632543c4a8bff344896dd9dc 061d322397e84ee0917f23ffa4741914 4051b9b901c24c12a477033faea7275c--061d322397e84ee0917f23ffa4741914 77f15ccd40c9498da9d17612441db847 RX(theta₁₀) 061d322397e84ee0917f23ffa4741914--77f15ccd40c9498da9d17612441db847 f7bc2afa2cbb414f9686f9e80a3c32cf RY(theta₁₃) 77f15ccd40c9498da9d17612441db847--f7bc2afa2cbb414f9686f9e80a3c32cf 75ad156abc2643a9a9cc9ce3ccbac203 RX(theta₁₆) f7bc2afa2cbb414f9686f9e80a3c32cf--75ad156abc2643a9a9cc9ce3ccbac203 f298f63b4b204b6b9f4520168f04d426 X 75ad156abc2643a9a9cc9ce3ccbac203--f298f63b4b204b6b9f4520168f04d426 f298f63b4b204b6b9f4520168f04d426--c4dd72a8e23841d8a5104dc500ce8bd1 c434015c72e940958a33cb7272609f95 f298f63b4b204b6b9f4520168f04d426--c434015c72e940958a33cb7272609f95 45758cccbc934c229ae971c056bc1653 RX(theta₁₉) c434015c72e940958a33cb7272609f95--45758cccbc934c229ae971c056bc1653 8003dd69322b4ab2abb6966cca282320 RY(theta₂₂) 45758cccbc934c229ae971c056bc1653--8003dd69322b4ab2abb6966cca282320 f4980036b42c465cbf24b470bdb6c6b1 RX(theta₂₅) 8003dd69322b4ab2abb6966cca282320--f4980036b42c465cbf24b470bdb6c6b1 b02449dbdf934d57aa70213f455a71f5 X f4980036b42c465cbf24b470bdb6c6b1--b02449dbdf934d57aa70213f455a71f5 b02449dbdf934d57aa70213f455a71f5--21a08bb235e84e39bdb0ff306078fd88 590a5dfaf01b4e5e9ed877d3c5e48fcc b02449dbdf934d57aa70213f455a71f5--590a5dfaf01b4e5e9ed877d3c5e48fcc 590a5dfaf01b4e5e9ed877d3c5e48fcc--df6a5243b81c4aa89ec5fe66c84bdfd2 38f9fe1fb5c847fe9c90361b5bdc25bc f7991fadd28a4fc69aa6e0b820a1b087 RX(3.0*acos(x)) f309a989b5ab45f69be79b1330e30ba9--f7991fadd28a4fc69aa6e0b820a1b087 c1514c6dd3544950a6fb90aadcda0604 RX(theta₂) f7991fadd28a4fc69aa6e0b820a1b087--c1514c6dd3544950a6fb90aadcda0604 018523135d1f40df922fd4427a181430 RY(theta₅) c1514c6dd3544950a6fb90aadcda0604--018523135d1f40df922fd4427a181430 04586be679454365837251f4111055b3 RX(theta₈) 018523135d1f40df922fd4427a181430--04586be679454365837251f4111055b3 e6b4e2692ffa45ec84a8fed0ed95a679 04586be679454365837251f4111055b3--e6b4e2692ffa45ec84a8fed0ed95a679 2312be5e631b40f1a024bb7d9acf4527 X e6b4e2692ffa45ec84a8fed0ed95a679--2312be5e631b40f1a024bb7d9acf4527 2312be5e631b40f1a024bb7d9acf4527--061d322397e84ee0917f23ffa4741914 6766a37a697a4552b56130b00330b545 RX(theta₁₁) 2312be5e631b40f1a024bb7d9acf4527--6766a37a697a4552b56130b00330b545 5454864b8eac4d9c861e58e48b81244c RY(theta₁₄) 6766a37a697a4552b56130b00330b545--5454864b8eac4d9c861e58e48b81244c 925b575acfad456fbaf7b75f07c40942 RX(theta₁₇) 5454864b8eac4d9c861e58e48b81244c--925b575acfad456fbaf7b75f07c40942 c10f3b5da3f548d58cf8e65c60c77598 925b575acfad456fbaf7b75f07c40942--c10f3b5da3f548d58cf8e65c60c77598 7c45ca79ca9b40d2a6b6d2a005415844 X c10f3b5da3f548d58cf8e65c60c77598--7c45ca79ca9b40d2a6b6d2a005415844 7c45ca79ca9b40d2a6b6d2a005415844--c434015c72e940958a33cb7272609f95 4fffd8b6b0ec4ef282daaf7604f4549f RX(theta₂₀) 7c45ca79ca9b40d2a6b6d2a005415844--4fffd8b6b0ec4ef282daaf7604f4549f 24f11584f15c40e78dea83664e875dcb RY(theta₂₃) 4fffd8b6b0ec4ef282daaf7604f4549f--24f11584f15c40e78dea83664e875dcb bb5d347dee1943b9ab32bf2e9ff4c9e1 RX(theta₂₆) 24f11584f15c40e78dea83664e875dcb--bb5d347dee1943b9ab32bf2e9ff4c9e1 fdfd34fb10f441b3a7c3ec4cd7067d8c bb5d347dee1943b9ab32bf2e9ff4c9e1--fdfd34fb10f441b3a7c3ec4cd7067d8c 2c82c5c067f8403486e8ad165dde14a0 X fdfd34fb10f441b3a7c3ec4cd7067d8c--2c82c5c067f8403486e8ad165dde14a0 2c82c5c067f8403486e8ad165dde14a0--590a5dfaf01b4e5e9ed877d3c5e48fcc 2c82c5c067f8403486e8ad165dde14a0--38f9fe1fb5c847fe9c90361b5bdc25bc

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-12-05T14:32:13.961082 image/svg+xml Matplotlib v3.9.3, 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-12-05T14:32:22.004647 image/svg+xml Matplotlib v3.9.3, https://matplotlib.org/

References