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_fea53b8e7cad418a83f070702bea04b6 HEA cluster_5447c0e53bd7488c964b4d938f933f2e Tower Chebyshev FM b2262ed34d5e430fa7f2b70d3009a64e 0 6a13ecbf4d6b43c3bf95f1bf39075bcd RX(1.0*acos(x)) b2262ed34d5e430fa7f2b70d3009a64e--6a13ecbf4d6b43c3bf95f1bf39075bcd b606a3a598dc41f798cd4b0b650c3964 1 1610d73e6b414000afc43ceb3ea0170b RX(theta₀) 6a13ecbf4d6b43c3bf95f1bf39075bcd--1610d73e6b414000afc43ceb3ea0170b 5b2c73f43b514e36b36f78f4787a30ad RY(theta₃) 1610d73e6b414000afc43ceb3ea0170b--5b2c73f43b514e36b36f78f4787a30ad 45c170bdbd114bfabe24f94c2f8a418e RX(theta₆) 5b2c73f43b514e36b36f78f4787a30ad--45c170bdbd114bfabe24f94c2f8a418e b3617bdc76024aeba2f4226ec2db2846 45c170bdbd114bfabe24f94c2f8a418e--b3617bdc76024aeba2f4226ec2db2846 f4bdca9f3c744de5841eab20e7de2d1a b3617bdc76024aeba2f4226ec2db2846--f4bdca9f3c744de5841eab20e7de2d1a ace63286b29741658178c98d622848ae RX(theta₉) f4bdca9f3c744de5841eab20e7de2d1a--ace63286b29741658178c98d622848ae fc6e5ba0adc346999140cb15e98677b5 RY(theta₁₂) ace63286b29741658178c98d622848ae--fc6e5ba0adc346999140cb15e98677b5 147527813cd445629f33ef4214ade12e RX(theta₁₅) fc6e5ba0adc346999140cb15e98677b5--147527813cd445629f33ef4214ade12e fe90721cd6d74212b6f4ad8b411c96d9 147527813cd445629f33ef4214ade12e--fe90721cd6d74212b6f4ad8b411c96d9 e2b90f7f01594defa32a50e3849e0547 fe90721cd6d74212b6f4ad8b411c96d9--e2b90f7f01594defa32a50e3849e0547 5186780a5bad479b866ca36eda9d7327 RX(theta₁₈) e2b90f7f01594defa32a50e3849e0547--5186780a5bad479b866ca36eda9d7327 836e450e29224f8c88a7190726f2e806 RY(theta₂₁) 5186780a5bad479b866ca36eda9d7327--836e450e29224f8c88a7190726f2e806 2a3883bbc14f4036b896a27efc4ceb09 RX(theta₂₄) 836e450e29224f8c88a7190726f2e806--2a3883bbc14f4036b896a27efc4ceb09 890b6452b8d34c75af4890e190c22e47 2a3883bbc14f4036b896a27efc4ceb09--890b6452b8d34c75af4890e190c22e47 33437d2be99a4719bebd9c14dd3d8d6f 890b6452b8d34c75af4890e190c22e47--33437d2be99a4719bebd9c14dd3d8d6f eb91745d1b7a4a38a0d48eabf90c65ae 33437d2be99a4719bebd9c14dd3d8d6f--eb91745d1b7a4a38a0d48eabf90c65ae fba7a6149e614bcd8850c8e7dad01285 94c8f56064b243a39f389035dc094342 RX(2.0*acos(x)) b606a3a598dc41f798cd4b0b650c3964--94c8f56064b243a39f389035dc094342 3ede9670c8de4db0a0084a56555ef44c 2 6ca00c72bd45422aa1c09e7485a0eb4b RX(theta₁) 94c8f56064b243a39f389035dc094342--6ca00c72bd45422aa1c09e7485a0eb4b 54724dd89ae74257a8db3378c8368d7f RY(theta₄) 6ca00c72bd45422aa1c09e7485a0eb4b--54724dd89ae74257a8db3378c8368d7f cc643e4e292f4299be5df777acf61293 RX(theta₇) 54724dd89ae74257a8db3378c8368d7f--cc643e4e292f4299be5df777acf61293 6a6fc750ff4c484eb9212b0ccd5b86db X cc643e4e292f4299be5df777acf61293--6a6fc750ff4c484eb9212b0ccd5b86db 6a6fc750ff4c484eb9212b0ccd5b86db--b3617bdc76024aeba2f4226ec2db2846 84ec4ba4b6a74bb4bcb4117cd12fda25 6a6fc750ff4c484eb9212b0ccd5b86db--84ec4ba4b6a74bb4bcb4117cd12fda25 2bab2ff2dba74ef49f70507d1b479e6b RX(theta₁₀) 84ec4ba4b6a74bb4bcb4117cd12fda25--2bab2ff2dba74ef49f70507d1b479e6b f6b18b01a4c6478da20acccad3ad5189 RY(theta₁₃) 2bab2ff2dba74ef49f70507d1b479e6b--f6b18b01a4c6478da20acccad3ad5189 a74e192a72864f4fa5b7bbfb806f4004 RX(theta₁₆) f6b18b01a4c6478da20acccad3ad5189--a74e192a72864f4fa5b7bbfb806f4004 efc0c74cef9440f7a2f2f5f5c88d96c0 X a74e192a72864f4fa5b7bbfb806f4004--efc0c74cef9440f7a2f2f5f5c88d96c0 efc0c74cef9440f7a2f2f5f5c88d96c0--fe90721cd6d74212b6f4ad8b411c96d9 5e792129a69f4327bc5d298610359235 efc0c74cef9440f7a2f2f5f5c88d96c0--5e792129a69f4327bc5d298610359235 a29d2dde051b415991d52734b3a525a7 RX(theta₁₉) 5e792129a69f4327bc5d298610359235--a29d2dde051b415991d52734b3a525a7 f9fb080f032f4361838ddf8074ab6abf RY(theta₂₂) a29d2dde051b415991d52734b3a525a7--f9fb080f032f4361838ddf8074ab6abf 8a3caa53098c4371b0f3fb9995db4f5d RX(theta₂₅) f9fb080f032f4361838ddf8074ab6abf--8a3caa53098c4371b0f3fb9995db4f5d b0ad14e16ef7481dab6e4d8d164f03f6 X 8a3caa53098c4371b0f3fb9995db4f5d--b0ad14e16ef7481dab6e4d8d164f03f6 b0ad14e16ef7481dab6e4d8d164f03f6--890b6452b8d34c75af4890e190c22e47 58b6a6ed7b264dfdbe56117991dad623 b0ad14e16ef7481dab6e4d8d164f03f6--58b6a6ed7b264dfdbe56117991dad623 58b6a6ed7b264dfdbe56117991dad623--fba7a6149e614bcd8850c8e7dad01285 8ae07a6bed36400ca1bd344100851d3d e31eceb26936468ab7284dc946ca6265 RX(3.0*acos(x)) 3ede9670c8de4db0a0084a56555ef44c--e31eceb26936468ab7284dc946ca6265 ac16444aec4a483e9565a4cbd715ecf0 RX(theta₂) e31eceb26936468ab7284dc946ca6265--ac16444aec4a483e9565a4cbd715ecf0 77416b5a23064da0a2b086dae0a75afa RY(theta₅) ac16444aec4a483e9565a4cbd715ecf0--77416b5a23064da0a2b086dae0a75afa c99ecf015d9a46c593a5a282eb9ca787 RX(theta₈) 77416b5a23064da0a2b086dae0a75afa--c99ecf015d9a46c593a5a282eb9ca787 64df0651a2f646fd8d68c8e96b72c940 c99ecf015d9a46c593a5a282eb9ca787--64df0651a2f646fd8d68c8e96b72c940 752f41fd0bad4a28b0d0bc82bce896c6 X 64df0651a2f646fd8d68c8e96b72c940--752f41fd0bad4a28b0d0bc82bce896c6 752f41fd0bad4a28b0d0bc82bce896c6--84ec4ba4b6a74bb4bcb4117cd12fda25 aa5dc1e1956f4cdcb475cbeac7b78aa7 RX(theta₁₁) 752f41fd0bad4a28b0d0bc82bce896c6--aa5dc1e1956f4cdcb475cbeac7b78aa7 e5e4aa69a38f421d92fc363e4f0ca3cc RY(theta₁₄) aa5dc1e1956f4cdcb475cbeac7b78aa7--e5e4aa69a38f421d92fc363e4f0ca3cc 374d52a28d5b4b4897f303bba851965b RX(theta₁₇) e5e4aa69a38f421d92fc363e4f0ca3cc--374d52a28d5b4b4897f303bba851965b 83768ec834b943f68aab6b0154d7dac4 374d52a28d5b4b4897f303bba851965b--83768ec834b943f68aab6b0154d7dac4 577cf34fb8214851ba462c8b6fec3fd2 X 83768ec834b943f68aab6b0154d7dac4--577cf34fb8214851ba462c8b6fec3fd2 577cf34fb8214851ba462c8b6fec3fd2--5e792129a69f4327bc5d298610359235 f56c9ed7725c4bbe924ec672169c3577 RX(theta₂₀) 577cf34fb8214851ba462c8b6fec3fd2--f56c9ed7725c4bbe924ec672169c3577 4f6904e1c3214b83b0b78fe38e6c641a RY(theta₂₃) f56c9ed7725c4bbe924ec672169c3577--4f6904e1c3214b83b0b78fe38e6c641a 4b5423ea63f04a32804f15f3285af4a4 RX(theta₂₆) 4f6904e1c3214b83b0b78fe38e6c641a--4b5423ea63f04a32804f15f3285af4a4 04f9d228f7304b27ba56e43f2f1a9530 4b5423ea63f04a32804f15f3285af4a4--04f9d228f7304b27ba56e43f2f1a9530 923dbd9c8dd749ac8408f84530589fe0 X 04f9d228f7304b27ba56e43f2f1a9530--923dbd9c8dd749ac8408f84530589fe0 923dbd9c8dd749ac8408f84530589fe0--58b6a6ed7b264dfdbe56117991dad623 923dbd9c8dd749ac8408f84530589fe0--8ae07a6bed36400ca1bd344100851d3d

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")
2025-02-14T12:51:29.636034 image/svg+xml Matplotlib v3.10.0, 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")
2025-02-14T12:51:36.907057 image/svg+xml Matplotlib v3.10.0, https://matplotlib.org/

References