# qGANS & differentiable circuits

I am working on qGANs project where I have a real circuit, a generator and a discriminator. Below is a summary of the functionality of my code for a simple 1 qubit example:

1. The real circuit generates a real state - |R>
2. The generator circuit generates a fake state given some input parameters - |F>
3. The discriminator performs measurements of expectation values on the real and the fake states.
4. The discriminator first creates the hermitian phi where $$\phi = \alpha_0 X_0 + \alpha_1 Y_0 + \alpha_2 Z_0 + \alpha_3 I_0$$ and the expectation value <F|$$\phi$$|F> is measured.
5. The discriminator then creates hermitian psi where $$\psi = \beta_0 X_0 + \beta_1 Y_0 + \beta_2 Z_0 + \beta_3 I_0$$ and the expectation value <R|$$\psi$$|R> is measured.
6. Some other quantities are calculated such as term1 and term2 and a loss function is defined.
7. The parameters to be tuned to minimise the loss function for the generator are the input parameters to the generator circuit
8. The parameters to be tuned to minimise the loss for the discriminator are the $$\alpha$$'s & $$\beta$$'s

Below is a short version of my code:

import pennylane as qml
from pennylane import numpy as np
import tensorflow as tf
import itertools
import functools
import operator
from openfermion import QubitOperator
from openfermion import get_sparse_operator
from sympy.physics.quantum.dagger import Dagger
from pennylane import qchem
from scipy.linalg import expm

n = 1 #no of qubits

lamb = np.float(2)
s = np.exp(-1 / (2 * lamb)) - 1
cst1 = (s / 2 + 1) ** 2

dev1 = qml.device('default.qubit', wires=1) #real
dev2 = qml.device('default.qubit', wires=1) #generator

def real(real_weights, wires):

qml.RX(real_weights[0], wires=0)
qml.RY(real_weights[1], wires=0)
qml.RZ(real_weights[2], wires=0)

def generator(gen_weights, wires):

qml.RX(gen_weights[0], wires=0)
qml.RY(gen_weights[1], wires=0)
qml.RZ(gen_weights[2], wires=0)

def discriminator(disc_weights, real_weights, gen_weights):

disc_weights_phi = disc_weights[:len(disc_weights)//2]
disc_weights_psi = disc_weights[len(disc_weights)//2:]

tuple_list_phi = [(weight, tup[0], tup[1]) for weight, tup in zip(disc_weights_phi, itertools.product(['X', 'Y', 'Z'], range(n)))]
tuple_list_psi = [(weight, tup[0], tup[1]) for weight, tup in zip(disc_weights_psi, itertools.product(['X', 'Y', 'Z'], range(n)))]

measurements_phi =  functools.reduce(operator.add, (weight * QubitOperator(f'{a}{n}') for weight, a, n in tuple_list_phi))
measurements_psi =  functools.reduce(operator.add, (weight * QubitOperator(f'{a}{n}') for weight, a, n in tuple_list_psi))

iden_phi =  functools.reduce(operator.add, ( (disc_weights_phi[len(disc_weights_phi)-1] ) * QubitOperator(" ") ))
iden_psi =  functools.reduce(operator.add, ( (disc_weights_psi[len(disc_weights_psi)-1]) * QubitOperator(" ") ))

phi_of = operator.add(iden_phi, measurements_phi) #phi in the openfermion manner
psi_of = operator.add(iden_psi, measurements_psi) #psi in openfermion manner

phi_matrix = get_sparse_operator(phi_of).todense()
psi_matrix = get_sparse_operator(psi_of).todense()

phi = qchem.convert_observable(phi_of)
psi = qchem.convert_observable(psi_of)

phi_cost = qml.VQECost(generator, phi, dev1, interface="tf")
psi_cost = qml.VQECost(real, psi, dev2, interface="tf")

phi_exp = phi_cost(gen_weights)
psi_exp = psi_cost(real_weights)

gen_sv = dev2.state
real_sv = dev1.state

A = expm(np.float(-1 / lamb) * phi_matrix)
B = expm(np.float(1 / lamb) * psi_matrix)

term1 = np.matmul(Dagger(gen_sv) , np.matmul(A, gen_sv))
term2 = np.matmul(Dagger(real_sv), np.matmul(B, real_sv))

regterm = (lamb / np.e * (cst1 * term1 * term2)).item()

return psi_exp, phi_exp, regterm

def disc_loss(disc_weights):

psi_exp, phi_exp, regterm = discriminator(disc_weights, real_weights, gen_weights)
loss = np.real(psi_exp - phi_exp - regterm)

return -loss

def gen_loss(gen_weights):

generator(gen_weights)

psi_exp , phi_exp , regterm = discriminator(disc_weights, real_weights, gen_weights)
loss = np.real(psi_exp - phi_exp - regterm)

return loss

real_weights = np.random.uniform(0, 2*np.pi , n*3)
init_gen_weights = np.random.uniform(0, 2*np.pi , n*3)
init_disc_weights = np.random.uniform(0, 2 , 8)

gen_weights = init_gen_weights
disc_weights = init_disc_weights

opt = tf.keras.optimizers.SGD(0.4)
opt.minimize(gen_loss, gen_weights)

opt = tf.keras.optimizers.SGD(0.4)
opt.minimize(disc_loss, disc_weights)



When I run the optimiser, I am getting: ValueError: Passed in object of type <class 'pennylane.numpy.tensor.tensor'>, not tf.Tensor error. I think this is because the generator circuit is not a qnode and hence not differentiable. But if I do define a qnode above the generator function, the VQE cost method here which I use to calculate the expectation values in the discriminator doesnâ€™t like that. The same error arises for opt.minimize(disc_loss, disc_weights) . Any ideas on how to proceed. Thanks in advance.

I believe the weights that are passed to opt.minimize must be TensorFlow variables. You can simply wrap the init_gen_weights and init_disc_weights with tf.Variable:

gen_weights = tf.Variable(init_gen_weights)
disc_weights = tf.Variable(init_disc_weights)


Also, to get the actual values from the variables you would need to extract them by writing e.g. disc_weights.value() inside of your discriminator function. Furthermore, as you also mentioned, you will need to wrap the functions containing the specific quantum circuits with a QNode, although these cannot contain any classical processing, so you need to be careful when choosing which function should be wrapped.

The QGan tutorial might be useful for you to check out as well, if you havenâ€™t already done so.

Let me know if this solves the issues youâ€™re having!

Thank you for some helpful initial tips. I decided to approach the problem again from first principles as I realised that there isnâ€™t a direct one to one mapping between my existing code and its implementation on PennyLane.

My current code looks like this:

import pennylane as qml
import numpy as np
import tensorflow as tf

#function to calculate fidelity between 2 states
def fidelity(rsv, gsv):

rsv_conj = np.conj(rsv)
fid = np.real(sum(rsv_conj*gsv) * np.conj(sum(rsv_conj*gsv)))

return fid

dev1 = qml.device('default.qubit', wires=1) #real
dev2 = qml.device('default.qubit', wires=1) #generator

@qml.qnode(dev1, interface="tf")
def real_circuit(weights):

qml.RX(weights[0], wires=0)
qml.RY(weights[1], wires=0)
qml.RZ(weights[2], wires=0)

return qml.expval(qml.PauliX(0)) #how can one also calculate Y(0), Z(0) and I(0)?

@qml.qnode(dev2, interface="tf")
def gen_circuit(weights):

qml.RX(weights[0], wires=0)
qml.RY(weights[1], wires=0)
qml.RZ(weights[2], wires=0)

return qml.expval(qml.PauliX(0)) #how can one also calculate Y(0), Z(0) and I(0)?

def discriminator(disc_weights, real_exp, gen_exp):

psi = disc_weights[0] * real_exp #real_exp is the output from the real circuit
phi = disc_weights[1] * gen_exp #gen_exp is the output from the generator circuit

return psi, phi

def disc_loss(disc_weights):

psi, phi = discriminator(disc_weights, real_exp, gen_exp)
loss = psi - phi

return -loss

def gen_loss(gen_weights):

gen_exp = gen_circuit(gen_weights)

psi, phi = discriminator(disc_weights, real_exp, gen_exp)
loss = psi - phi

return loss

real_weights = np.random.uniform(0, 2*np.pi, 3)
real_exp = real_circuit(real_weights)
real_sv = dev1.state

init_gen_weights = np.random.uniform(0, 2*np.pi, 3)
gen_weights = tf.Variable(init_gen_weights)
gen_exp = gen_circuit(gen_weights)
gen_sv = dev2.state

init_disc_weights = np.random.uniform(0, 2*np.pi, 2)
disc_weights = tf.Variable(init_disc_weights)

print('initial fidelity is ' , fidelity(real_sv, gen_sv))
print('real weights ', real_weights)
print('initial gen weights are ', init_gen_weights)
print('initial disc weights are ', init_disc_weights)

opt = tf.keras.optimizers.SGD(0.4)

cost = lambda: disc_loss(disc_weights)

for step in range(50):
opt.minimize(cost, disc_weights)
if step % 5 == 0:
cost_val = cost().numpy()
print("Step {}: cost = {}".format(step, cost_val))

cost = lambda: gen_loss(gen_weights)

for step in range(50):
opt.minimize(cost, gen_weights)
if step % 5 == 0:
cost_val = cost().numpy()
print("Step {}: cost = {}".format(step, cost_val))



This works and outputs a loss which is minimised using the optimiser but is, of course, incorrect as a bunch of terms are missing which go into my calculation of the loss function.

My question now is: how can I also calculate the Y, Z and I expectation values as the output of the real and generator circuits?
I have looked at qml.map() and added the following section:

dev1 = qml.device('default.qubit', wires=1) #real

@qml.qnode(dev1, interface="tf")
def real_circuit(weights):

qml.RX(weights[0], wires=0)
qml.RY(weights[1], wires=0)
qml.RZ(weights[2], wires=0)

return qml.expval(qml.PauliX(0)) #how can one also calculate Y(0), Z(0) and I(0)?

obs_list = [qml.PauliX(0) , qml.PauliY(0), qml.PauliZ(0) , qml.Identity(0)]
qnodes = qml.map(real_circuit, obs_list, dev1, measure="expval")
real_weights = np.random.uniform(0, 2*np.pi, 3)
qnodes(real_weights)



but get the following error : QuantumFunctionError: Unknown quantum function parameter 'wires'.

Any ideas/pointers on how to solve this? Thanks once again.

Your approach seems to be on track. You can use qml.map() to create a collection of QNodes (aka a QNodeCollection) containing all the QNodes returning the corresponding observable measurement in the obs_list.

When using qml.map() you only need to supply a template function, and not a QNode, skipping the decorator as well as the final measurement return (since qml.map() will create those for you; one for each observable in obs_list).

Also note that the template must have the following signature:

template(params, wires, **kwargs)


as described in the documentation for qml.map().

The following works for me:

dev1 = qml.device('default.qubit', wires=1) #real

def real_circuit(weights, wires, **kwargs):

qml.RX(weights[0], wires=0)
qml.RY(weights[1], wires=0)
qml.RZ(weights[2], wires=0)

obs_list = [qml.PauliX(0) , qml.PauliY(0), qml.PauliZ(0) , qml.Identity(0)]
qnodes = qml.map(real_circuit, obs_list, dev1, measure="expval")
real_weights = np.random.uniform(0, 2*np.pi, 3)
qnodes(real_weights)


Let me know if you have any other issues or questions!

Once again, thank you for following up with my issues.

The problem I am now having is something to do with the conversion of a tf.Variable() to a numpy array using .numpy() and vice versa. I have also tried .value() but the qnodes() function doesnâ€™t seem to accept that.
Minimising the disc_loss works but the gen_loss throws a ValueError: No gradients provided for any variable: ['Variable:0'].  error.

I have provided my code below which may give you a better insight. Any feedback is much appreciated.

import pennylane as qml
import numpy as np
import tensorflow as tf

dev1 = qml.device('default.qubit', wires=1) #real
dev2 = qml.device('default.qubit', wires=1) #generator

obs_list = [qml.PauliX(0) , qml.PauliY(0), qml.PauliZ(0) , qml.Identity(0)]

def real_circuit(weights, wires, **kwargs):

qml.RX(weights[0], wires=0)
qml.RY(weights[1], wires=0)
qml.RZ(weights[2], wires=0)

qnodes_real = qml.map(real_circuit, obs_list, dev1, measure="expval",  interface="tf")

def gen_circuit(weights, wires, **kwargs):

qml.RX(weights[0], wires=0)
qml.RY(weights[1], wires=0)
qml.RZ(weights[2], wires=0)

qnodes_gen = qml.map(gen_circuit, obs_list, dev2, measure="expval",  interface="tf")

def discriminator(disc_weights, real_exp, gen_exp):

psi = disc_weights[0] * real_exp[0] + disc_weights[1] * real_exp[1] + disc_weights[2] * real_exp[2] + disc_weights[3] * real_exp[3]
phi = disc_weights[4] * gen_exp[0] + disc_weights[5] * gen_exp[1] + disc_weights[6] * gen_exp[2] + disc_weights[7] * gen_exp[3]

return psi, phi

def disc_loss(disc_weights):

psi, phi = discriminator(disc_weights, real_exp, gen_exp)
loss = psi - phi

return -loss

def gen_loss(gen_weights):

gen_exp = qnodes_gen(gen_weights.numpy())

psi, phi = discriminator(disc_weights, real_exp, gen_exp)
loss = psi - phi

return loss

real_weights = np.random.uniform(0, 2*np.pi, 3)
real_exp = qnodes_real(real_weights)#calculate the expectation values from the real circuit

init_gen_weights = np.random.uniform(0, 2*np.pi, 3)
gen_weights = tf.Variable(init_gen_weights)
gen_exp = qnodes_gen(gen_weights.numpy()) #calculate the expectation values from the generator

init_disc_weights = np.random.uniform(0, 2*np.pi, 8)
disc_weights = tf.Variable(init_disc_weights)

print('real weights ', real_weights)
print('initial gen weights are ', init_gen_weights)
print('initial disc weights are ', init_disc_weights)

opt = tf.keras.optimizers.SGD(0.4)

cost = lambda: disc_loss(disc_weights)

for step in range(50):
opt.minimize(cost, disc_weights)
if step % 5 == 0:
cost_val = cost().numpy()
print("Step {}: cost = {}".format(step, cost_val))

cost = lambda: gen_loss(gen_weights)

for step in range(50):
opt.minimize(cost, gen_weights)
if step % 5 == 0:
cost_val = cost().numpy()
print("Step {}: cost = {}".format(step, cost_val))



Ignore the above issue I raised. The qnodes() function does accept `tf.Variableâ€™ but for some reason yesterday, it was throwing up an error. The problem now seems to have been resolved. Thankyou.

2 Likes