# Source code for qubiter.CGateSEO_writer

from qubiter.SEO_writer import *
from qubiter.SEO_simulator import *
from qubiter.CktEmbedder import *
from qubiter.BitVector import *
import itertools as it
import collections as co

[docs]class CGateSEO_writer(SEO_writer):
"""
This class is a child of SEO_writer. When one_line=True, it writes a
single line for an c_u2 (controlled U(2) gate).  When one_line=False,
it writes an expansion of that gate. When expand_1c_u2=True, the c_u2 is
expanded so that its sub-component 1c_u2 (singly controlled U(2) gates)
are expanded into cnots and qubit rotations. If expand_1c_u2=False,
the 1c_u2 are not expanded.

If we say a gate is controlled, it may have 1 or more controls (it might
be singly or multiply controlled).

Global phase factors are ignored, so expansions equal the original line
up to a phase factor.

References
----------

1. CGateSEO_writer.pdf, by Robert Tucci, included with Qubiter source
code.

Attributes
----------
do_checking : bool
Does some checking of algebra
expand_1c_u2 : bool
When this is True, the c_u2 (controlled U(2) gate) is expanded so
that its sub-component 1c_u2 (uni controlled U(2) gates) are
expanded into cnots and qubit rotations. If this is False, the 1c_u2
are not expanded.
one_line : bool
When this is True, it writes a single line for the c_u2 ( controlled
U(2) gate). When False, it writes an expansion of that gate.

"""

[docs]    def __init__(self, file_prefix, emb,
one_line=True, expand_1c_u2=False, do_checking=False,
verbose=False, **kwargs):
"""
Constructor

Parameters
----------
file_prefix : str
emb : CktEmbedder
one_line : bool
expand_1c_u2 : bool
do_checking : bool
verbose : bool

Returns
-------

"""
self.one_line = one_line
self.expand_1c_u2 = expand_1c_u2
self.do_checking = do_checking
self.verbose = verbose
SEO_writer.__init__(self, file_prefix, emb, **kwargs)

[docs]    @staticmethod
def su2_mat_prod(su2_pair1, su2_pair2):
"""
An SU(2) matrix can be expressed as exp(i*theta*sig_n) where theta
is a real number and sig_n = n \cdot sigma. Here n is a 3 dim real
UNIT vector and sigma = [sigx, sigy, sigz], where sigx, sigy and
sigz are the 3 Pauli matrices. We define the su2_pair of this SU(2)
matrix as the list [theta, n], with n expressed as a numpy array.
One can prove by Taylor expansion that

exp(i*theta*sig_n) = c + i*sig_n*s

where c = cos(theta) and s = sin(theta).

This subroutine maps su2_pair1, su2_pair2 --> su2_pair

where

exp(i*theta*sig_n) = exp(i*theta1*sig_n1) exp(i*theta2*sig_n2)

Parameters
----------
su2_pair1 : [float, np.array]
su2_pair of left matrix in matrix product

su2_pair2 : [float, np.array]
su2_pair of right matrix in matrix product

Returns
-------
[float, np.array]

"""

theta1, n1 = su2_pair1
theta2, n2 = su2_pair2
s1, c1 = np.sin(theta1), np.cos(theta1)
s2, c2 = np.sin(theta2), np.cos(theta2)
n = s1*c2*n1 + s2*c1*n2 - s1*s2*np.cross(n1, n2)
mag = np.linalg.norm(n)
n /= mag
c = c1*c2 - s1*s2*np.dot(n1, n2)
s = mag
theta = np.arctan2(s, c)
return [theta, n]

[docs]    def write_1c_u2(self, tar_bit_pos, trol_bit_pos, rads_list, delta=None):
"""
Writes an expansion of an 1c_u2 (singly controlled U(2) matrix). In
general, such an expansion will contain 3 cnots, but for special
cases taken here into account, it's possible to get away with using
only 2 or 1 cnots.

Parameters
----------
tar_bit_pos : int
target bit position
trol_bit_pos : int
control bit position

rads_list : list[float]
list of 3 angles in radians. If it equals [radx, rady, radz],
then U(2) gate given by e^{i*delta} exp(i*(radx*sigx + rady*sigy
+ radz*sigz))

delta : float|None
U(2) gate being controlled equals e^{i*delta} times SU(2) gate

Returns
-------
None

"""
num_qbits = self.emb.num_qbits_bef
trols = Controls.new_single_trol(num_qbits, trol_bit_pos, True)
if not self.expand_1c_u2:
self.write_controlled_one_qbit_gate(
tar_bit_pos, trols, OneQubitGate.rot, rads_list)
return

def write_delta_rot():
if delta:
self.write_one_qbit_gate(
trol_bit_pos, OneQubitGate.rot_ax, [-delta/2, 3])

def write_cnot():
self.write_controlled_one_qbit_gate(
tar_bit_pos, trols, OneQubitGate.sigx)

def write_rot(rads_list1, herm_conj=False):
if not herm_conj:
rads_list2 = rads_list
else:
rads_list2 = list(-np.array(rads_list1))
self.write_one_qbit_gate(
tar_bit_pos, OneQubitGate.rot, rads_list2)

rads_x, rads_y, rads_z = rads_list
theta_w = np.sqrt(rads_x**2 + rads_y**2 + rads_z**2)
cw, sw = np.cos(theta_w), np.sin(theta_w)
wx, wy, wz = [rads_x/theta_w, rads_y/theta_w, rads_z/theta_w]
TOL = 1E-6

if abs(theta_w - np.pi/2) < TOL:  # 1 cnot, 0 or 2 target rots
if abs(wy) < TOL and abs(wz) < TOL and \
abs(delta + np.pi/2) < TOL:  # simple cnot, 0 target rots
write_cnot()
else:  # 1 cnot, 2 target rots
theta_a = np.pi/2
ax = np.sqrt((wx+1)/2)
# ax != 0 or else wy=wz=0, which was already considered
ay = wy/(2*ax)
az = wz/(2*ax)
rads_list_a = list(theta_a*np.array([ax, ay, az]))

write_rot(rads_list_a, herm_conj=True)
write_cnot()
write_rot(rads_list_a)
write_delta_rot()

if self.do_checking:
mat_w = np.matrix(OneQubitGate.rot(*rads_list))
mat_a = np.matrix(OneQubitGate.rot(*rads_list_a))
mat_sigx = np.matrix(OneQubitGate.sigx())
diff = mat_w - mat_a*mat_sigx*mat_a.getH()
err = np.linalg.norm(diff)
if err > TOL:
print("1 cnot, 2 rots")
print(diff)
assert False

elif abs(wx) < TOL:  # 2 cnots, 2 target rots
# this is the same as the general case (2 cnots, 3 target rots)
# with alp=beta
alp = np.arctan2(wz*sw, cw)/2
gamma = np.arctan2(sw*wy,
np.sqrt(cw**2 + (wz*sw)**2))
su2_pair_a = CGateSEO_writer.su2_mat_prod(
[alp, np.array([0, 0, 1])],
[gamma/2, np.array([0, 1, 0])])
rads_list_a = list(su2_pair_a[0]*su2_pair_a[1])

write_cnot()
write_rot(rads_list_a, herm_conj=True)
write_cnot()
write_rot(rads_list_a)
write_delta_rot()

if self.do_checking:
mat_w = np.matrix(OneQubitGate.rot(*rads_list))
mat_a = np.matrix(OneQubitGate.rot(*rads_list_a))
mat_sigx = np.matrix(OneQubitGate.sigx())
diff = mat_w - mat_a*mat_sigx*mat_a.getH()*mat_sigx
err = np.linalg.norm(diff)
if err > TOL:
print("2 cnot, 2 rots", rads_list)
print(diff)
assert False

else:  # 2 cnots, 3 target rots
theta1 = np.arctan2(wz*sw, cw)
theta2 = np.arctan2(wx, wy)
alp = (theta1 + theta2)/2
beta = (theta1 - theta2)/2
gamma = np.arctan2(sw*np.sqrt(wx**2 + wy**2),
np.sqrt(cw**2 + (wz*sw)**2))
su2_pair_a = CGateSEO_writer.su2_mat_prod(
[alp, np.array([0, 0, 1])],
[gamma/2, np.array([0, 1, 0])])
su2_pair_b = CGateSEO_writer.su2_mat_prod(
[-gamma/2, np.array([0, 1, 0])],
[-(alp+beta)/2, np.array([0, 0, 1])])
su2_pair_c = [(beta-alp)/2, np.array([0, 0, 1])]
rads_list_a = list(su2_pair_a[0]*su2_pair_a[1])
rads_list_b = list(su2_pair_b[0]*su2_pair_b[1])
rads_list_c = list(su2_pair_c[0]*su2_pair_c[1])

write_rot(rads_list_c)
write_cnot()
write_rot(rads_list_b)
write_cnot()
write_rot(rads_list_a)
write_delta_rot()

if self.do_checking:
mat_id = np.matrix(OneQubitGate.phase_fac(0.0))
mat_w = np.matrix(OneQubitGate.rot(*rads_list))
mat_a = np.matrix(OneQubitGate.rot(*rads_list_a))
mat_b = np.matrix(OneQubitGate.rot(*rads_list_b))
mat_c = np.matrix(OneQubitGate.rot(*rads_list_c))
mat_sigx = np.matrix(OneQubitGate.sigx())
diff = mat_w - mat_a*mat_sigx*mat_b*mat_sigx*mat_c
err = np.linalg.norm(diff)
if err > TOL:
print("2 cnot, 3 rots, identity 1")
print(diff)
assert False
diff = mat_id - mat_a*mat_b*mat_c
err = np.linalg.norm(diff)
if err > TOL:
print("2 cnot, 3 rots, identity 2")
print(diff)
assert False

# def write_gen_n_controlled_u2(self, n_index_list, rads_list, delta=None):
#     """
#     Writes an expansion for a U(2) matrix W(num_qbits-1) that is
#     controlled by a "generalized n" equal to GN = n(n_index_list). Thus,
#     the gate written by this function equals W(num_qbits-1)^GN.
#     Generalized n's are defined in the reference CGateExpander.pdf
#
#     Parameters
#     ----------
#     n_index_list : list[int]
#         indices of the generalized n
#
#     rads_list : list[float]
#         list of 3 angles in radians. If it equals [radx, rady, radz],
#         then U(2) gate given by e^{i*delta} exp(i*(radx*sigx + rady*sigy
#         + radz*sigz))
#
#     delta : float|None
#         U(2) gate being controlled equals e^{i*delta} times SU(2) gate
#
#     Returns
#     -------
#     None
#
#     """
#
#     num_qbits = self.emb.num_qbits_bef
#
#     for k in range(len(n_index_list)-1):
#         tar_pos = n_index_list[k+1]
#         trol_pos = n_index_list[k]
#         trols = Controls.new_single_trol(num_qbits, trol_pos, True)
#         self.write_controlled_one_qbit_gate(
#             tar_pos, trols, OneQubitGate.sigx)
#
#     self.write_1c_u2(num_qbits - 1, n_index_list[-1], rads_list, delta)
#
#     for k in reversed(range(len(n_index_list)-1)):
#         tar_pos = n_index_list[k+1]
#         trol_pos = n_index_list[k]
#         trols = Controls.new_single_trol(num_qbits, trol_pos, True)
#         self.write_controlled_one_qbit_gate(
#             tar_pos, trols, OneQubitGate.sigx)
#
# def write_internal(self, rads_list, delta=None):
#     """
#     This internal function is used in write() and is less general than
#     the latter. It expands an c_u2 into a product of several
#     "generalized n" controlled U(2) gates.
#
#     Parameters
#     ----------
#     rads_list : list[float]
#         list of 3 angles in radians. If it equals [radx, rady, radz],
#         then U(2) gate given by e^{i*delta} exp(i*(radx*sigx + rady*sigy
#         + radz*sigz))
#
#     delta : float|None
#         U(2) gate being controlled equals e^{i*delta} times SU(2) gate
#
#     Returns
#     -------
#     None
#
#     """
#     num_qbits = self.emb.num_qbits_bef
#     for num_boxes in range(1, num_qbits):
#         for comb in it.combinations(range(0, num_qbits-1), num_boxes):
#             n_index_list = sorted(list(comb), reverse=True)
#             sign = 1
#             if len(comb) % 2 == 0:
#                 sign = -1
#             new_rads_list = list(
#                 sign*np.array(rads_list)/(1 << (num_qbits-2)))
#             if delta:
#                 new_delta = sign*delta/(1 << (num_qbits-2))
#             else:
#                 new_delta = None
#             self.write_gen_n_controlled_u2(
#                 n_index_list, new_rads_list, new_delta)

[docs]    def write_internal(self, rads_list, delta=None):
"""
This internal function is used in write() and is less general than
the latter. It expands an c_u2 into a product of 1c_u2 with
intervening cnots.

In the CGateSEO_writer.pdf documentation, we show that any c_u2 can
be expanded into a product of several "generalized n" controlled U(
2) gates of the form  W(num_qbits-1)^GN, wherein U(2) matrix W(
num_qbits-1) is controlled by a "generalized n" equal to GN = n(
n_index_list)

Since the factors W(num_qbits-1)^GN in the product commute amongst
themselves, it is possible and convenient to order them in Gray code
order (Qubiter knows about Gray Code via its class BitVector).
Ordering them in Gray Code allows this function to cancel some cnots
from adjacent GN.

An earlier version of this function, now commented, did not use Gray
Code and used more cnots than this one.

Parameters
----------
rads_list : list[float]
list of 3 angles in radians. If it equals [radx, rady, radz],
then U(2) gate given by e^{i*delta} exp(i*(radx*sigx + rady*sigy
+ radz*sigz))

delta : float|None
U(2) gate being controlled equals e^{i*delta} times SU(2) gate

Returns
-------
None

"""
num_qbits = self.emb.num_qbits_bef
num_trols = num_qbits-1
max_f = (1 << num_trols)-1

def write_cnot(tar_bpos, trol_bpos):
trol = Controls.new_single_trol(num_qbits, trol_bpos, True)
self.write_controlled_one_qbit_gate(tar_bpos, trol,
OneQubitGate.sigx)

def write_cnot_stair(bvec):
tar_bpos = bvec.find_rightmost_T_bit()
trol_bpos = tar_bpos
while True:
trol_bpos = bvec.find_T_bit_to_left_of(trol_bpos)
if trol_bpos == -1:
break
write_cnot(tar_bpos, trol_bpos)

cur_bvec = BitVector(num_trols, 0)
prev_bvec = BitVector(num_trols, 0)
f, lazy = 0, 0
f, lazy = BitVector.lazy_advance(f, lazy)
cur_bvec.dec_rep = lazy
while f <= max_f:
if self.verbose:
print("\nf, lazy", f, lazy)
self.write_NOTA(str(prev_bvec) + "->" + str(cur_bvec))
sign = 1
if cur_bvec.get_num_T_bits() % 2 == 0:
sign = -1
new_rads_list = list(
sign*np.array(rads_list)/(1 << (num_qbits-2)))
if delta:
new_delta = sign*delta/(1 << (num_qbits-2))
else:
new_delta = None

diff_bvec = BitVector.new_with_T_on_diff(cur_bvec, prev_bvec)
diff_bpos = diff_bvec.find_rightmost_T_bit()
min_prev_bpos = prev_bvec.find_rightmost_T_bit()
min_cur_bpos = cur_bvec.find_rightmost_T_bit()

if f > 1:  # first 1c_u2 has not cnots preceding it
if min_cur_bpos == min_prev_bpos:
write_cnot(min_cur_bpos, diff_bpos)
else:
write_cnot_stair(prev_bvec)
write_cnot_stair(cur_bvec)

u2_trol_bpos = min_cur_bpos
self.write_1c_u2(
num_qbits - 1, u2_trol_bpos, new_rads_list, new_delta)
prev_bvec = BitVector.copy(cur_bvec)
f, lazy = BitVector.lazy_advance(f, lazy)
cur_bvec.dec_rep = lazy

[docs]    def write_hads(self, trol_kinds, herm_conj=False):
"""
Writes a chain of cnots that are useful when some of the controls of
the c_u2 being considered are n_bar = P_0 = |0><0| instead of
n = P_1 = |1><1|. We are using the identity sigx n sigx = nbar
to convert n's to nbar's.

Parameters
----------
trol_kinds : list[bool]
A list of control kinds. True for n=P_1 and False for n_bar=P_0
herm_conj : bool
When this is True, writes Hermitian conjugate of expansion.

Returns
-------
None

"""
num_trols = len(trol_kinds)
if not herm_conj:
range1 = range(num_trols)
else:
range1 = reversed(range(num_trols))
for k in range1:
if not trol_kinds[k]:
self.write_one_qbit_gate(num_trols-k-1, OneQubitGate.sigx)

[docs]    def write(self, trol_kinds, u2_fun, fun_arg_list=None):
"""
This is the most general function of this class. All other functions
of the class are mostly internal and are called by this function.
This function achieves the main goal of the class, which is to give
various expansions of an c_u2 (controlled U(2) matrix). For
one_line=True, this function just calls
write_controlled_one_qbit_gate() of the parent class. For
one_line=False, it gives an expansion of the c_u2.

Parameters
----------
trol_kinds : list[bool]
list of control types. Type is False if nbar=P_0 and True if n=P_1
u2_fun : function
One of the functions in class OneQubitGate
fun_arg_list : list[int|float]
list of arguments of u2_fun

Returns
-------
None

"""
num_qbits = self.emb.num_qbits_bef
tar_bit_pos = num_qbits-1
num_trols = num_qbits-1
assert len(trol_kinds) == num_trols
trols = Controls(num_qbits)
trols.bit_pos_to_kind = {k: trol_kinds[num_qbits-k-2]
for k in range(0, num_qbits-1)}
trols.refresh_lists()

if self.one_line or num_trols == 0:
self.write_controlled_one_qbit_gate(tar_bit_pos,
trols, u2_fun, fun_arg_list)
return

# insert opening Hadamards for controls equal to n_bar = |0><0|
self.write_hads(trols.kinds)

if u2_fun == OneQubitGate.P_0_phase_fac:
rads = fun_arg_list[0]
self.write_internal([0, 0, rads / 2], rads / 2)
elif u2_fun == OneQubitGate.P_1_phase_fac:
rads = fun_arg_list[0]
self.write_internal([0, 0, -rads / 2], rads / 2)
elif u2_fun == OneQubitGate.sigx:
if num_qbits == 2:
# If it's a CNOT, no expansion necessary
# Control must be set to True because
# opening and closing Hadamards take care of False
trols1 = Controls.new_single_trol(num_qbits, 0, True)
self.write_controlled_one_qbit_gate(
tar_bit_pos, trols1, OneQubitGate.sigx)
else:
self.write_internal([np.pi / 2, 0, 0], -np.pi / 2)
elif u2_fun == OneQubitGate.sigy:
self.write_internal([0, np.pi / 2, 0], -np.pi / 2)
elif u2_fun == OneQubitGate.sigz:
self.write_internal([0, 0, np.pi / 2], -np.pi / 2)
elif u2_fun == OneQubitGate.had2:
rads = np.pi/(2*np.sqrt(2))
self.write_internal([rads, 0, rads], -np.pi / 2)
elif u2_fun == OneQubitGate.rot_ax:
rads = fun_arg_list[0]
axis = fun_arg_list[1]
if axis == 1:
self.write_internal([rads, 0, 0])
elif axis == 2:
self.write_internal([0, rads, 0])
elif axis == 3:
self.write_internal([0, 0, rads])
else:
assert False
elif u2_fun == OneQubitGate.rot:
self.write_internal(fun_arg_list)
elif u2_fun == OneQubitGate.u2:
self.write_internal(fun_arg_list[1:], fun_arg_list[0])
else:
assert False, "writing an unsupported controlled gate"

# insert closing Hadamards for controls equal to n_bar = |0><0|
self.write_hads(trols.kinds, herm_conj=True)

if __name__ == "__main__":
from qubiter.SEO_MatrixProduct import *
from qubiter.OneQubitGate import *

def main():

num_qbits_bef = 4
num_qbits_aft = 5
bit_map = list(range(num_qbits_bef))
emb = CktEmbedder(num_qbits_bef, num_qbits_aft, bit_map)

# trol_kinds in ZL convention
trol_kinds = [True, False, False]

wr = CGateSEO_writer('cgate_expansions', emb,
do_checking=True, verbose=False)

u2_fun_to_fun_arg_list = co.OrderedDict((
(OneQubitGate.P_0_phase_fac, [np.pi/3]),
(OneQubitGate.P_1_phase_fac, [np.pi/3]),
(OneQubitGate.sigx, None),
(OneQubitGate.sigy, None),
(OneQubitGate.sigz, None),
(OneQubitGate.had2, None),
(OneQubitGate.rot_ax, [np.pi/3, 2]),
(OneQubitGate.rot, [np.pi/3, np.pi/6, np.pi/3])
))

for u2_fun, fun_arg_list in u2_fun_to_fun_arg_list.items():

wr.write_NOTA('--------new u2 gate --------------------------')
for one_line in [True, False]:
wr.one_line = one_line
if one_line:
wr.write(trol_kinds, u2_fun, fun_arg_list)
else:
for expand_1c_u2 in [False, True]:
wr.expand_1c_u2 = expand_1c_u2
wr.write_NOTA(
'--------expand_1c_u2=' + str(expand_1c_u2))
print("\n", u2_fun,
"one_line=", one_line, "expand=", expand_1c_u2)
wr.write(trol_kinds, u2_fun, fun_arg_list)

wr.close_files()

# a check that an expansion multiplies to original
num_qbits = 5
emb = CktEmbedder(num_qbits, num_qbits)
# trol_kinds in ZL convention
trol_kinds = [True, False, False, False]
file_prefix = 'cgate_expan_mat_prod'

wr = CGateSEO_writer(file_prefix, emb)

u2_fun = OneQubitGate.rot_ax
rads = np.pi/3

wr.one_line = True
wr.write_NOTA("one line=True-----------")
wr.write(trol_kinds, u2_fun, [rads, 2])

wr.write_NOTA("herm. conj, one line=False-----------")
wr.one_line = False
wr.write(trol_kinds, u2_fun, [-rads, 2])

wr.close_files()

mp = SEO_MatrixProduct(file_prefix, num_qbits)
id_mat = np.diag(np.ones((1 << num_qbits,)))
err = np.linalg.norm(mp.prod_arr - id_mat)
print("err=", err)
main()