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:
It admits an exact solution:
Our goal will be to find this solution for .
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 gradDefining the loss function
Section titled “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 criterioncriterion = 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_lossDifferent 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
Section titled “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, chainfrom qadence import QNN, QuantumCircuit, Zfrom qadence.types import BasisSet, ReuploadScaling
n_qubits = 3depth = 3
# Feature mapfm = feature_map( n_qubits = n_qubits, param = "x", fm_type = BasisSet.CHEBYSHEV, reupload_scaling = ReuploadScaling.TOWER,)
# Ansatzansatz = hea(n_qubits = n_qubits, depth = depth)
# Observableobservable = 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)Training the model
Section titled “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 and compute the loss function for those points.
n_epochs = 200n_points = 10
xmin = -0.99xmax = 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 are only picked from since we are using a Chebyshev feature map, and derivative of diverges at and .
Plotting the results
Section titled “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")Clearly, the result is not optimal.
Improving the solution
Section titled “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")