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:
- The real circuit generates a real state - |R>
- The generator circuit generates a fake state given some input parameters - |F>
- The discriminator performs measurements of expectation values on the real and the fake states.
- 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.
- 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.
- Some other quantities are calculated such as term1 and term2 and a loss function is defined.
- The parameters to be tuned to minimise the loss function for the generator are the input parameters to the generator circuit
- 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.