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_ebb26aa3c9cf4127a6dce820bd64e772 HEA cluster_a4865a205d084d99a6293fa867c5bb33 Tower Chebyshev FM c3c4d582e80447eaac40ebac3eecbabb 0 f14624d48c8545869fc756170ca206cc RX(1.0*acos(x)) c3c4d582e80447eaac40ebac3eecbabb--f14624d48c8545869fc756170ca206cc 25473ffe1e434f9a9550c8396f15b4f2 1 fe332a9ca4034cabaee2c182eec2c445 RX(theta₀) f14624d48c8545869fc756170ca206cc--fe332a9ca4034cabaee2c182eec2c445 599d382108b94ee4b0b43cdf1bed5bc3 RY(theta₃) fe332a9ca4034cabaee2c182eec2c445--599d382108b94ee4b0b43cdf1bed5bc3 2e00228c88614c298632056a37e7fe3d RX(theta₆) 599d382108b94ee4b0b43cdf1bed5bc3--2e00228c88614c298632056a37e7fe3d 1ea1db9917f244abb03cff3ec774030d 2e00228c88614c298632056a37e7fe3d--1ea1db9917f244abb03cff3ec774030d be456e0b86d848d28014e48e8c662077 1ea1db9917f244abb03cff3ec774030d--be456e0b86d848d28014e48e8c662077 2c0b2cf3d4864c2c83d4f558a8602c45 RX(theta₉) be456e0b86d848d28014e48e8c662077--2c0b2cf3d4864c2c83d4f558a8602c45 5fd1f1f8fd304b57ba49f7bf3402b5dc RY(theta₁₂) 2c0b2cf3d4864c2c83d4f558a8602c45--5fd1f1f8fd304b57ba49f7bf3402b5dc 8288bb11c9884beeb15450077679b38b RX(theta₁₅) 5fd1f1f8fd304b57ba49f7bf3402b5dc--8288bb11c9884beeb15450077679b38b dce1a97d4ce2437a959fcd411534f0fc 8288bb11c9884beeb15450077679b38b--dce1a97d4ce2437a959fcd411534f0fc 9f4b7cd5a76e4ce8a2b83778f05518ae dce1a97d4ce2437a959fcd411534f0fc--9f4b7cd5a76e4ce8a2b83778f05518ae 06348a1fc9ac42a38927920775eba12e RX(theta₁₈) 9f4b7cd5a76e4ce8a2b83778f05518ae--06348a1fc9ac42a38927920775eba12e c34217f13dd845db8312cdfcf552a90b RY(theta₂₁) 06348a1fc9ac42a38927920775eba12e--c34217f13dd845db8312cdfcf552a90b 96c1f2b22f9541f398ae15a1f260763a RX(theta₂₄) c34217f13dd845db8312cdfcf552a90b--96c1f2b22f9541f398ae15a1f260763a 3794da6751c24779bc09fa661cadc0b0 96c1f2b22f9541f398ae15a1f260763a--3794da6751c24779bc09fa661cadc0b0 541f83f88952406293aaeb2fa2b17ebf 3794da6751c24779bc09fa661cadc0b0--541f83f88952406293aaeb2fa2b17ebf d214663d6c804f25ae0af3148a1a3254 541f83f88952406293aaeb2fa2b17ebf--d214663d6c804f25ae0af3148a1a3254 2c1500c6ef694707b98da2d50150985d 48ddcfd8b13a4c78a93aba4325c78840 RX(2.0*acos(x)) 25473ffe1e434f9a9550c8396f15b4f2--48ddcfd8b13a4c78a93aba4325c78840 7c2aca9917f946c382097e3732424558 2 4c88340a02944713ba488603b131e38f RX(theta₁) 48ddcfd8b13a4c78a93aba4325c78840--4c88340a02944713ba488603b131e38f 79834f2f958140d19e62a0668db0c771 RY(theta₄) 4c88340a02944713ba488603b131e38f--79834f2f958140d19e62a0668db0c771 e8ad645d525f4290bae7886e9b08df93 RX(theta₇) 79834f2f958140d19e62a0668db0c771--e8ad645d525f4290bae7886e9b08df93 9bf9bda115474f328b50bebea71ac0b9 X e8ad645d525f4290bae7886e9b08df93--9bf9bda115474f328b50bebea71ac0b9 9bf9bda115474f328b50bebea71ac0b9--1ea1db9917f244abb03cff3ec774030d 3f18843286a54973b700a1e6115b1768 9bf9bda115474f328b50bebea71ac0b9--3f18843286a54973b700a1e6115b1768 f396f99f00364b8fba83c2e3cdc8592d RX(theta₁₀) 3f18843286a54973b700a1e6115b1768--f396f99f00364b8fba83c2e3cdc8592d f725252e0c5748b28dd43c39dbc484af RY(theta₁₃) f396f99f00364b8fba83c2e3cdc8592d--f725252e0c5748b28dd43c39dbc484af ffd4e643fbb742139eb3ec4653a767db RX(theta₁₆) f725252e0c5748b28dd43c39dbc484af--ffd4e643fbb742139eb3ec4653a767db 3f8abcc300534009a2fa0d5425da1c36 X ffd4e643fbb742139eb3ec4653a767db--3f8abcc300534009a2fa0d5425da1c36 3f8abcc300534009a2fa0d5425da1c36--dce1a97d4ce2437a959fcd411534f0fc 357ddc1195dd4d5d9024a5f5ffe457ce 3f8abcc300534009a2fa0d5425da1c36--357ddc1195dd4d5d9024a5f5ffe457ce f97169d250b7457394667ade03d2c6ce RX(theta₁₉) 357ddc1195dd4d5d9024a5f5ffe457ce--f97169d250b7457394667ade03d2c6ce 5d8a0e946d744b7aa226a1d88747bc6e RY(theta₂₂) f97169d250b7457394667ade03d2c6ce--5d8a0e946d744b7aa226a1d88747bc6e bcfd1194aa424057bc75932b63f991ce RX(theta₂₅) 5d8a0e946d744b7aa226a1d88747bc6e--bcfd1194aa424057bc75932b63f991ce b0b2e288659d4058b75942929776fa6d X bcfd1194aa424057bc75932b63f991ce--b0b2e288659d4058b75942929776fa6d b0b2e288659d4058b75942929776fa6d--3794da6751c24779bc09fa661cadc0b0 fb669bec2fec4ab09e0028f7917a0b05 b0b2e288659d4058b75942929776fa6d--fb669bec2fec4ab09e0028f7917a0b05 fb669bec2fec4ab09e0028f7917a0b05--2c1500c6ef694707b98da2d50150985d b4a8d59682eb4d1782ea49392d54281d b54b123e3b014c578b2b4fac2eacae07 RX(3.0*acos(x)) 7c2aca9917f946c382097e3732424558--b54b123e3b014c578b2b4fac2eacae07 a5f9b560fa9a460fbd7a8d4f8f0dbf54 RX(theta₂) b54b123e3b014c578b2b4fac2eacae07--a5f9b560fa9a460fbd7a8d4f8f0dbf54 15748b60cc1b4cdfb72d10319ce0ff99 RY(theta₅) a5f9b560fa9a460fbd7a8d4f8f0dbf54--15748b60cc1b4cdfb72d10319ce0ff99 0295f344bc5d4bfe87212fea48004bcc RX(theta₈) 15748b60cc1b4cdfb72d10319ce0ff99--0295f344bc5d4bfe87212fea48004bcc 83a3fa538ff14fb59ddcaa7ad25d3736 0295f344bc5d4bfe87212fea48004bcc--83a3fa538ff14fb59ddcaa7ad25d3736 305430a8d5cb46dfbc35206563436078 X 83a3fa538ff14fb59ddcaa7ad25d3736--305430a8d5cb46dfbc35206563436078 305430a8d5cb46dfbc35206563436078--3f18843286a54973b700a1e6115b1768 c9075976867c4e6f946f0483e19d2837 RX(theta₁₁) 305430a8d5cb46dfbc35206563436078--c9075976867c4e6f946f0483e19d2837 b04abafcc104456c8231d4a53394fd7a RY(theta₁₄) c9075976867c4e6f946f0483e19d2837--b04abafcc104456c8231d4a53394fd7a f976f33399304daa9474b4676e2ff33c RX(theta₁₇) b04abafcc104456c8231d4a53394fd7a--f976f33399304daa9474b4676e2ff33c 9586cf6900164c79a764739b4b326bf8 f976f33399304daa9474b4676e2ff33c--9586cf6900164c79a764739b4b326bf8 a11d75d97add4f5892333ccdc7fb69cb X 9586cf6900164c79a764739b4b326bf8--a11d75d97add4f5892333ccdc7fb69cb a11d75d97add4f5892333ccdc7fb69cb--357ddc1195dd4d5d9024a5f5ffe457ce 0d7e50c293384e5b80c3ba2082040c80 RX(theta₂₀) a11d75d97add4f5892333ccdc7fb69cb--0d7e50c293384e5b80c3ba2082040c80 101c35d67e644ec78f8fe572ed43e4f3 RY(theta₂₃) 0d7e50c293384e5b80c3ba2082040c80--101c35d67e644ec78f8fe572ed43e4f3 4155c39b243e401d926c5a8f7155f4d7 RX(theta₂₆) 101c35d67e644ec78f8fe572ed43e4f3--4155c39b243e401d926c5a8f7155f4d7 fbd913e8358846eabde86521eec24733 4155c39b243e401d926c5a8f7155f4d7--fbd913e8358846eabde86521eec24733 0f9fd3879aca48c68e722dbaeba2a23e X fbd913e8358846eabde86521eec24733--0f9fd3879aca48c68e722dbaeba2a23e 0f9fd3879aca48c68e722dbaeba2a23e--fb669bec2fec4ab09e0028f7917a0b05 0f9fd3879aca48c68e722dbaeba2a23e--b4a8d59682eb4d1782ea49392d54281d

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-01-23T14:19:35.770638 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-01-23T14:19:42.149973 image/svg+xml Matplotlib v3.10.0, https://matplotlib.org/

References