Variational Circuits for Solving Differential Equations

Hi everyone! I’m new to Pennylane, and I’m trying to solve ODEs by adapting this and this tutorial. As you will see, I have essentially changed the loss function.

Let’s say that I want to solve: y'(x) = x, with y(0)=y_0. Let QNN(x)=y(x) be the output of my circuit. We then define the following loss function L:
L = \sqrt{(QNN(0)-y_0)^2} + \sqrt{\sum_i \left(\frac{dQNN(x_i)}{dx} - y'(x_i)\right)^2}
We want to minimize L.

Here’s the code that I’m currently using:

import pennylane as qml
import tensorflow as tf
from pennylane import numpy as np
from pennylane.optimize import AdamOptimizer

dev_fock = qml.device("strawberryfields.fock", wires=1, cutoff_dim=10)

#for a single qumode, we define the following layer of our variational circuit

def layer(v):
    # Matrix multiplication of input layer
    qml.Rotation(v[0], wires=0)
    qml.Squeezing(v[1], 0.0, wires=0)
    qml.Rotation(v[2], wires=0)

    # Bias
    qml.Displacement(v[3], 0.0, wires=0)

    # Element-wise nonlinear transformation
    qml.Kerr(v[4], wires=0)

@qml.qnode(dev_fock, diff_method="parameter-shift")
def quantum_neural_net(var, x):
    # Encode input x into quantum state
    qml.Displacement(x, 0.0, wires=0)

    # "layer" subcircuits
    for v in var:
        layer(v)

    return qml.expval(qml.X(0))

#here we initialize the weights    
np.random.seed(0)
num_layers = 4
var_init = 0.05 * np.random.randn(num_layers, 5, requires_grad=True)
var = var_init

#variables
f0 = 0
qnn0 = quantum_neural_net(var,0)
inf_s = np.sqrt(np.finfo(np.float32).eps)
learning_rate = 0.01
training_steps = 5000
batch_size = 100
display_step = 500

# Given EDO
def f(x):
    return x

def qnn(x):
    return quantum_neural_net(var,x)

# Custom loss function to approximate the derivatives
def custom_loss(x):
    summation = []
    summation.append((qnn0-f0)**2)
    
    for x in np.linspace(-1,1,10):
        dQNN = (qnn(x+inf_s)-qnn(x))/inf_s
        summation.append((dQNN - f(x))**2)
    
    return tf.sqrt(tf.reduce_mean(tf.abs(summation)))

opt = AdamOptimizer(0.01, beta1=0.9, beta2=0.999)

X = np.linspace(-1, 1, 50)
Y = np.linspace(-1, 1, 50)

for it in range(50):
    (var, _, _), _cost = opt.step_and_cost(custom_loss, X) #error here
    print("Iter: {:5d} | Cost: {:0.7f} ".format(it, _cost))

#output of our model

x_pred = np.linspace(-1, 1, 50)
predictions = [quantum_neural_net(var, x_) for x_ in x_pred]

So, I’m having problems when dealing with my custom_loss function. Is there a feasible way to fix it? Also, if you know a different approach and could share it with me, that would be great as well :slight_smile:

Thanks in advance!

Hi @rodfer, welcome to the forum!

What specific error or issue are you getting?

We don’t have a specific function for solving ODEs.

I found this paper which solves ODEs with PennyLane and StrawberryFields. Maybe it can give you some insight on how to solve this problem.

Please feel free to copy your error message or detail your problem so that I can help you better.

Hi @CatalinaAlbornoz, thanks for replying!
Yes, I’m trying to replicate some results of that paper.

I have managed to fix the error. Here’s my code now:

import pennylane as qml
import matplotlib.pyplot as plt
from pennylane import numpy as np
from pennylane.optimize import AdamOptimizer

#we'll use the Strawberry Fields simulator, this time with only one qumode (wire)

dev_fock = qml.device("strawberryfields.fock", wires=1, cutoff_dim=10)

#for a single qumode, we define the following layer of our variational circuit

def layer(v):
    # Matrix multiplication of input layer
    qml.Rotation(v[0], wires=0)
    qml.Squeezing(v[1], 0.0, wires=0)
    qml.Rotation(v[2], wires=0)

    # Bias
    qml.Displacement(v[3], 0.0, wires=0)

    # Element-wise nonlinear transformation
    qml.Kerr(v[4], wires=0)

#we first encode the input into a quantum state via Displacement Embedding
#the output is the expectation value of x-quadrature

@qml.qnode(dev_fock, diff_method="parameter-shift")
def quantum_neural_net(var, x):
    # Encode input x into quantum state
    qml.Displacement(x, 0.0, wires=0)

    # "layer" subcircuits
    for v in var:
        layer(v)

    return qml.expval(qml.X(0))

#declaring the initial value of the angles (var)

np.random.seed(0)
num_layers = 4
var_init = 0.05 * np.random.randn(num_layers, 5, requires_grad=True)
var = var_init

#variables

f0 = 0
qnn0 = quantum_neural_net(var,0)
e = 10**(-5)

#defining the ODE
def f(x):
    return 1

def qnn(var,x):
    return quantum_neural_net(var,x)

# Loss function to approximate the derivatives
def loss(var):
    L = 0
    
    for x in np.linspace(-1,1,10):
        dQNN = (qnn(var,x+e)-qnn(var,x))/e
        L = L + (dQNN - f(x))**2
        
    L = np.sqrt(L)
    L = L + np.sqrt((qnn0-f0)**2)
    
    return L

opt = AdamOptimizer(0.01, beta1=0.9, beta2=0.999)

for it in range(100):
    var, _loss = opt.step_and_cost(loss, var)
    print("Iter: {:5d} | Loss: {:0.7f} ".format(it, _loss))

And then I plot the result (the function y(x) that I was looking for):

x_pred = np.linspace(-1, 1, 50)
predictions = [quantum_neural_net(var, x_) for x_ in x_pred]
t = np.linspace(-1, 1, 50)
y = t
plt.figure()
plt.scatter(x_pred, predictions, color="green")
plt.plot(t,y,'b')
plt.xlabel("x")
plt.ylabel("f(x)")
plt.tick_params(axis="both", which="major")
plt.tick_params(axis="both", which="minor")
plt.show()

In this case, I solved dy/dx = 1, y(0)=0. I’m aiming for y(x) = x, -1 \leq x \leq 1.
After 500 iterations (the loss function reaches a plateau way before that, though), I got the following result:

y%3Dx

The blue line is the function y(x)=x itself, and the green dots correspond to my output. So, here are the 2 main questions now:

  1. I got those “shifted” functions for every example that I tried so far (y=x, x^2, \cos x,...). Do you see any mistakes in my code (e.g. loss function) that could explain such a shift?

  2. To compute the derivative of my output (dQNN(x)/dx), I used the definition, i.e.,
    \frac{dQNN}{dx} = \lim_{\epsilon \to 0}\frac{QNN(x+\epsilon) - QNN(x)}{\epsilon}
    Are there other possibilities to calculate the derivative? I think there might be room for improvement here.

Moreover, if you have any further suggestions, I would love to hear them as well. Thank you!

Hi @rodfer, I’ve noticed that if you change the constant in var_init to 0.2 you can improve your result.

This makes me think that there is something fundamentally wrong because the initial parameters shouldn’t be this determinant in the output. My thoughts are that because you only compare the value of the function at point 0 (L = L + np.sqrt((qnn0-f0)**2)) maybe it’s too little information for the network to optimize. It may be something entirely different too so you can try 2 things:

1 - Try using more datapoints of the function within the loss function for training
2 - Try to simplify your neural network as much as possible and slowly add complexity to it until you understand the root cause of this problem.

Please let me know if this helps!

Hi @CatalinaAlbornoz. Thanks for your comments.
I’ve updated the loss function like this:

# Loss function to approximate the derivatives
def loss(var):
    L = 0
    qnn0 = qnn(var,0)
    
    for x in np.linspace(-1,1,50):
        dQNN = (qnn(var,x+e)-qnn(var,x))/e
        L = L + (dQNN - f(x))**2
        
    L = np.sqrt(L)
    L = L + np.sqrt((qnn0-f0)**2)
    
    return L 

Which allowed me to get better results, such as the following simulation of dy/dx = x^2, y(0) = 0. It looks better, but still not good enough.

cubic

Your suggestion on increasing the data points within the loss function has actually improved my training - it went from L\approx0.06 (10 points) to L\approx0.03 (50 points). However, the total running time has also increased substantially.

I’m now working on new ways to compute the derivative of QNN - trying to use pennylane’s param_shift_cv and similar methods from PyTorch. I believe the derivative is now the main issue regarding the loss function (and the model accuracy).

Thanks again!

Hi @rodfer, I’m glad you got such a great improvement!

One thing you can try to speed up your simulation is to use diff_method="backprop" or diff_method="adjoint" instead of “parameter-shift”. I’m not 100% sure that they will work, but if they do then they will be much faster. Note though that these differentiation methods don’t work on actual hardware, only on simulators.

You can also try qml.grad for your gradient.

Let me know if this helps!

Hey @CatalinaAlbornoz, thank you!
Yes, I have compared my current approach (finite differences) to other methods (qml.grad and param_shift_cv) and the results were quite similar. After changing a couple of details, I got approximately the same values (L \approx 0.03). For some reason the qml.grad method requires more steps to reach this value - still don’t know why this is the case.

Anyway, I guess now I’ll just play around with the other parameters (e.g. number of layers) to improve the training. Thanks again for your comments. A substantial progress has been made indeed haha

1 Like