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_c70fcad854fe4309b7938fd0d2a2aeb4 HEA cluster_24daed42f7834076ad0ceb8bbe7de8c0 Tower Chebyshev FM 44f23130fd094e1a8e431434112336ec 0 99d08af739a84cca96866e581de49830 RX(1.0*acos(x)) 44f23130fd094e1a8e431434112336ec--99d08af739a84cca96866e581de49830 5fabda8f27fc4c21b833384736364a01 1 42632b8182c2455dbf7d6163e3c16394 RX(theta₀) 99d08af739a84cca96866e581de49830--42632b8182c2455dbf7d6163e3c16394 d58a95e52245491ab887acf39cb90fa3 RY(theta₃) 42632b8182c2455dbf7d6163e3c16394--d58a95e52245491ab887acf39cb90fa3 03dd7d06b6774366898ea6711c0fde80 RX(theta₆) d58a95e52245491ab887acf39cb90fa3--03dd7d06b6774366898ea6711c0fde80 3456c18d31f7495b87ac969cd40b3430 03dd7d06b6774366898ea6711c0fde80--3456c18d31f7495b87ac969cd40b3430 15b247cc0d9f482188b7576a45d8cf0d 3456c18d31f7495b87ac969cd40b3430--15b247cc0d9f482188b7576a45d8cf0d 9e3fce91184544f3899e9674f1dc43cc RX(theta₉) 15b247cc0d9f482188b7576a45d8cf0d--9e3fce91184544f3899e9674f1dc43cc 09cdee0139264e9f9e67fb38ff3615e0 RY(theta₁₂) 9e3fce91184544f3899e9674f1dc43cc--09cdee0139264e9f9e67fb38ff3615e0 3126af8cbbdf48b59e9bfb556a1bc39c RX(theta₁₅) 09cdee0139264e9f9e67fb38ff3615e0--3126af8cbbdf48b59e9bfb556a1bc39c ca0aa4eaeebf479aa3c2741c136d1981 3126af8cbbdf48b59e9bfb556a1bc39c--ca0aa4eaeebf479aa3c2741c136d1981 c09979cab4a24d44b28c5b1ea0fc3720 ca0aa4eaeebf479aa3c2741c136d1981--c09979cab4a24d44b28c5b1ea0fc3720 80d8ea04de8f405c87855152fd453371 RX(theta₁₈) c09979cab4a24d44b28c5b1ea0fc3720--80d8ea04de8f405c87855152fd453371 4f70b59e776b4f7689f9370690533969 RY(theta₂₁) 80d8ea04de8f405c87855152fd453371--4f70b59e776b4f7689f9370690533969 ff8fda52da804902ae71e48a2c5ce2d3 RX(theta₂₄) 4f70b59e776b4f7689f9370690533969--ff8fda52da804902ae71e48a2c5ce2d3 c7a55f8756054665967de5443ab251d6 ff8fda52da804902ae71e48a2c5ce2d3--c7a55f8756054665967de5443ab251d6 b035b9edb8144f4d88e3b12c4f1713bb c7a55f8756054665967de5443ab251d6--b035b9edb8144f4d88e3b12c4f1713bb 7589304165ee40f7b6f096a15eef56fb b035b9edb8144f4d88e3b12c4f1713bb--7589304165ee40f7b6f096a15eef56fb 3e3b609d431b460b8968bbd25b374dcf 1077edc557104e76916e085e1ffef248 RX(2.0*acos(x)) 5fabda8f27fc4c21b833384736364a01--1077edc557104e76916e085e1ffef248 e7c7ab4867ee439cbea9f58715c60053 2 846b152919184caea3cee57a699fc7d6 RX(theta₁) 1077edc557104e76916e085e1ffef248--846b152919184caea3cee57a699fc7d6 7e9fb1c3ebd5406d8da29167fe7d8d96 RY(theta₄) 846b152919184caea3cee57a699fc7d6--7e9fb1c3ebd5406d8da29167fe7d8d96 a45418fc9c0145d4bd58602c70930ce0 RX(theta₇) 7e9fb1c3ebd5406d8da29167fe7d8d96--a45418fc9c0145d4bd58602c70930ce0 acabba83674f44b18d7b8dd0c00a282d X a45418fc9c0145d4bd58602c70930ce0--acabba83674f44b18d7b8dd0c00a282d acabba83674f44b18d7b8dd0c00a282d--3456c18d31f7495b87ac969cd40b3430 72ebfb5ea1ba4c789fc0c6d41ca6c75f acabba83674f44b18d7b8dd0c00a282d--72ebfb5ea1ba4c789fc0c6d41ca6c75f f35fbee3ddf8424ebdfa03d55200b4e2 RX(theta₁₀) 72ebfb5ea1ba4c789fc0c6d41ca6c75f--f35fbee3ddf8424ebdfa03d55200b4e2 942bbc6d2226499aa9fdd94dfd27591c RY(theta₁₃) f35fbee3ddf8424ebdfa03d55200b4e2--942bbc6d2226499aa9fdd94dfd27591c 10f58ff295644322a897b4067e1f09ec RX(theta₁₆) 942bbc6d2226499aa9fdd94dfd27591c--10f58ff295644322a897b4067e1f09ec 8479d17c98d740f1a6bb00efb37586a8 X 10f58ff295644322a897b4067e1f09ec--8479d17c98d740f1a6bb00efb37586a8 8479d17c98d740f1a6bb00efb37586a8--ca0aa4eaeebf479aa3c2741c136d1981 0ebdecb09e884ecd99e4163fb7075fb5 8479d17c98d740f1a6bb00efb37586a8--0ebdecb09e884ecd99e4163fb7075fb5 d0ad689ebc0549ebbea74b6250bc2dd7 RX(theta₁₉) 0ebdecb09e884ecd99e4163fb7075fb5--d0ad689ebc0549ebbea74b6250bc2dd7 0e2ef0a9ace343668fe316555597dec5 RY(theta₂₂) d0ad689ebc0549ebbea74b6250bc2dd7--0e2ef0a9ace343668fe316555597dec5 5e7f55bb70e84b459b665bce6a105b0a RX(theta₂₅) 0e2ef0a9ace343668fe316555597dec5--5e7f55bb70e84b459b665bce6a105b0a e31aa396234342d8aa2495e8816b0f8c X 5e7f55bb70e84b459b665bce6a105b0a--e31aa396234342d8aa2495e8816b0f8c e31aa396234342d8aa2495e8816b0f8c--c7a55f8756054665967de5443ab251d6 2770d7d6c90a4a21abe67c19c916f470 e31aa396234342d8aa2495e8816b0f8c--2770d7d6c90a4a21abe67c19c916f470 2770d7d6c90a4a21abe67c19c916f470--3e3b609d431b460b8968bbd25b374dcf b97a661f08bd46078218b99e69d218a1 56c9f7d143e8487bab71b44ba607c16a RX(3.0*acos(x)) e7c7ab4867ee439cbea9f58715c60053--56c9f7d143e8487bab71b44ba607c16a 11e5e40c20114acdbee0b42346f944df RX(theta₂) 56c9f7d143e8487bab71b44ba607c16a--11e5e40c20114acdbee0b42346f944df 83f7eded02ac47e7b5b36760f87f8164 RY(theta₅) 11e5e40c20114acdbee0b42346f944df--83f7eded02ac47e7b5b36760f87f8164 3d9c23d78f174bb495b97aa15c1c5b32 RX(theta₈) 83f7eded02ac47e7b5b36760f87f8164--3d9c23d78f174bb495b97aa15c1c5b32 ec64d682fcc84ba88a475a4850fd07ea 3d9c23d78f174bb495b97aa15c1c5b32--ec64d682fcc84ba88a475a4850fd07ea efac11183cba48a39093637e4ac57782 X ec64d682fcc84ba88a475a4850fd07ea--efac11183cba48a39093637e4ac57782 efac11183cba48a39093637e4ac57782--72ebfb5ea1ba4c789fc0c6d41ca6c75f 4fdad0c0c3b346cab41c2474c2d5aaae RX(theta₁₁) efac11183cba48a39093637e4ac57782--4fdad0c0c3b346cab41c2474c2d5aaae 49b60cbc57644974a9f4deae2b797aa3 RY(theta₁₄) 4fdad0c0c3b346cab41c2474c2d5aaae--49b60cbc57644974a9f4deae2b797aa3 a0ab178a861a47b8b65d8b927c7f841a RX(theta₁₇) 49b60cbc57644974a9f4deae2b797aa3--a0ab178a861a47b8b65d8b927c7f841a 871266cea54a4e8586daade6b3f712ce a0ab178a861a47b8b65d8b927c7f841a--871266cea54a4e8586daade6b3f712ce 9cbec9d6d5b54b7191bbb810605cfb89 X 871266cea54a4e8586daade6b3f712ce--9cbec9d6d5b54b7191bbb810605cfb89 9cbec9d6d5b54b7191bbb810605cfb89--0ebdecb09e884ecd99e4163fb7075fb5 e5f06befa7ea4496b955795f28633597 RX(theta₂₀) 9cbec9d6d5b54b7191bbb810605cfb89--e5f06befa7ea4496b955795f28633597 65cc6802259b49ffbcedabff3041384c RY(theta₂₃) e5f06befa7ea4496b955795f28633597--65cc6802259b49ffbcedabff3041384c 5f3002b84c734735b7648d0c76e41370 RX(theta₂₆) 65cc6802259b49ffbcedabff3041384c--5f3002b84c734735b7648d0c76e41370 9ed6ddbcea3d45f6b4ddd48a2409701f 5f3002b84c734735b7648d0c76e41370--9ed6ddbcea3d45f6b4ddd48a2409701f df16445a19fb427a83bd5a033f0f5916 X 9ed6ddbcea3d45f6b4ddd48a2409701f--df16445a19fb427a83bd5a033f0f5916 df16445a19fb427a83bd5a033f0f5916--2770d7d6c90a4a21abe67c19c916f470 df16445a19fb427a83bd5a033f0f5916--b97a661f08bd46078218b99e69d218a1

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-20T14:47:26.451541 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-20T14:47:34.313951 image/svg+xml Matplotlib v3.7.5, https://matplotlib.org/

References