#!/usr/bin/python
# -*- coding: utf-8 -*-
##
# bsf.py: Implementation of binary symplectic form.
##
# © 2012 Christopher E. Granade (cgranade@gmail.com) and
# Ben Criger (bcriger@gmail.com).
# This file is a part of the QuaEC project.
# Licensed under the AGPL version 3.
##
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
##
## IMPORTS ##
from sys import version_info
if version_info[0] == 3:
PY3 = True
from importlib import reload
elif version_info[0] == 2:
PY3 = False
else:
raise EnvironmentError("sys.version_info refers to a version of "
"Python neither 2 nor 3. This is not permitted. "
"sys.version_info = {}".format(version_info))
import itertools
import string
from numpy import s_, array, nonzero, logical_xor, bitwise_xor, bitwise_not, logical_and, bitwise_and, logical_not, all, matrix, hstack, vstack, zeros, eye, dot, empty
from numpy.linalg import det
from qecc.singletons import EmptyClifford
if PY3:
from .exceptions import *
from .PauliClass import *
from .CliffordClass import *
from . import utils as u
from . import bsf_decomp
from . import circuit
else:
from exceptions import *
from PauliClass import *
from CliffordClass import *
import utils as u
import bsf_decomp
import circuit
from functools import reduce
reload(bsf_decomp)
## ALL ##
__all__ = [
'BinarySymplecticVector',
# 'bitstring_to_letterstring',
'parity', 'bitwise_inner_product', 'all_pauli_bsvs', 'constrained_set',
'commute', 'xz_switch',
'BinarySymplecticMatrix',
'is_bsm_valid', 'bsmzeros', 'array_to_pauli', 'directsum'
]
## CLASSES ##
VALID_BITS=list(range(2))
[docs]class BinarySymplecticVector(object):
"""
Encapsulates a binary symplectic vector representing an element of the Pauli
group on :math:`n` qubits.
A new :class:`BinarySymplecticVector` can be constructed using either a
single NumPy array containing both the :math:`X` and :math:`Z` parts of the
binary symplectic vector. Alternatively, a new vector can be instantiated
using two NumPy arrays. For example, the following two invocations are
equivalent:
>>> import qecc
>>> import numpy as np
>>> bsv = qecc.BinarySymplecticVector(np.array([1, 0, 0, 0, 0, 0]))
>>> bsv = qecc.BinarySymplecticVector(np.array([1, 0, 0]), np.array([0, 0, 0]))
The :obj:`len` of a :class:`BinarySymplecticVector` is defined as the number
of qubits upon which the represented Pauli operator acts, and is thus half
of the length of a single array containing the same data.
"""
def __init__(self,*args):
if len(args) == 1:
nq = int(len(args[0]) / 2)
self._x = array(args[0][0:nq], dtype=int)
self._z = array(args[0][nq:2*nq], dtype=int)
elif len(args) == 2:
xstring,zstring = args
self._x=array(list(xstring), dtype='int')
self._z=array(list(zstring), dtype='int')
else:
raise ValueError('Wrong number of args.')
## MAGIC METHODS ##
def __len__(self):
"""Number of qubits on which ``self`` acts."""
return len(self._x)
def __repr__(self):
return "( {ex} | {zed} )".format(ex=" ".join(map(str, self._x)), zed=" ".join(map(str, self._z)))
def __eq__(self,other):
return all([a[0] == a[1] for a in zip(self._x+self._z, other._x + other._z)])
## PROPERTIES ##
@property
def x(self):
"""
Array containing the :math:`X` part of the binary symplectic vector.
:rtype: :class:`numpy.ndarray`, shape ``(2 * nq, )``.
>>> import qecc as q
>>> q.BinarySymplecticVector([1,0,0,0,1,0]).x
array([1, 0, 0])
"""
return self._x
@property
def z(self):
"""
Array containing the :math:`Z` part of the binary symplectic vector.
:rtype: :class:`numpy.ndarray`, shape ``(nq, )``.
>>> import qecc as q
>>> q.BinarySymplecticVector([1,0,0,0,1,0]).z
array([0, 1, 0])
"""
return self._z
## OTHER METHODS ##
[docs] def copy(self):
"""
Returns a copy of the binary symplectic vector such that mutations of
the copy do not affect this instance. For more details, see the
:meth:`numpy.ndarray.copy` method.
"""
return BinarySymplecticVector(self._x.copy(), self._z.copy())
[docs] def as_pauli(self):
"""
Returns an instance of :class:`qecc.Pauli` representing the same Pauli
operator as this vector. Note that phase information is not preserved
by the binary symplectic representation of the Pauli group, and so
``P.as_bsv().as_pauli()`` need not equal ``P``.
>>> import qecc as q
>>> pauli_with_phase=q.Pauli('IXXYZ',2)
>>> pauli_with_phase.as_bsv().as_pauli()
i^0 IXXYZ
"""
exes=bitstring_to_letterstring(self._x,'X')
zeds=bitstring_to_letterstring(self._z,'Z')
zedley=Pauli(zeds)
exeley=Pauli(exes)
assert type(zedley) is type(exeley)
pauli=zedley*exeley
return Pauli(pauli.op,0)
[docs] def bsip(self,other):
r"""
Returns the binary symplectic inner product :math:`u \odot v` of this
vector with another vector. Letting :math:`u = (a | b)` and
:math:`v = (c | d)`, :math:`u\odot v = a \cdot d + b \cdot c`.
>>> import qecc as q
>>> vector_a = q.BinarySymplecticVector([1,0,1],[0,1,1])
>>> vector_b = q.Pauli('YYZ').as_bsv()
>>> vector_a.bsip(vector_b)
1
"""
return int(not(commute(self,other)))
#FUNCTIONS FOR BSV CLASS
def bitstring_to_letterstring(bitstring,letter):
"""Internal function, replaces all instances of 1 in ``bitstring``
with the letter in ``letter``.
:param list bitstring: a list or tuple, whose entries are 0 or 1.
:param letter: a string with a single entry
:rtype: str
>>>
"""
outstring=''
for idx in range(len(bitstring)):
if bitstring[idx]==0:
outstring=outstring+('I')
elif bitstring[idx]==1:
outstring=outstring+(letter)
return outstring
[docs]def parity(bitarray):
"""
:arg list bitarray: a list containing integers of value 0 or 1.
:returns: True if bitarray is of odd parity, False if it is of even parity.
:rtype: bool
"""
return reduce(logical_xor,bitarray)
def bitwise_inner_product(v1,v2):
"""
Internal function, returns the bitwise inner product of two bitstrings.
The bitwise inner product is the sum (modulo 2) of the product of the
elements.
"""
return parity(bitwise_and(v1,v2))
[docs]def all_pauli_bsvs(nq):
r"""
Lists all the Paulis on ``nq`` qubits according to their binary symplectic
representations.
:param int nq: Number of qubits.
:returns: an iterator that yields the binary symplectic representations of each element of the Pauli group :math:`\mathcal{P}_n`.
>>> list(all_pauli_bsvs(1))
[( 0 | 0 ), ( 0 | 1 ), ( 1 | 0 ), ( 1 | 1 )]
"""
for idx_x in itertools.product([0,1],repeat=nq):
for idx_z in itertools.product([0,1],repeat=nq):
yield(BinarySymplecticVector(idx_x,idx_z))
[docs]def constrained_set(pauli_array_input,logical_array_input):
r"""
Given a set of constraints of the form :math:`P_i \odot Q = b_i`, with
each :math:`P_i` a Pauli operator and each :math:`b_i` a bit, yields an
iterator onto Pauli operators :math:`Q` such that all constraints are
satisfied.
:type pauli_array_input: :obj:`list` of :class:`qecc.Pauli` instances.
:param pauli_array_input: Constraint operators :math:`P_i`.
:type logical_array_input: :class:`numpy.ndarray` of `dtype=int` and shape
``(len(pauli_array_input), )``.
:param logical_array_input: Constraint values :math:`b_i`.
>>> import qecc as q
>>> list(q.constrained_set(map(lambda s: q.Pauli(s).as_bsv(), ['XY','ZZ']),[1,0]))
[( 0 0 | 0 1 ), ( 0 0 | 1 0 ), ( 1 1 | 0 0 ), ( 1 1 | 1 1 )]
"""
nq=len(pauli_array_input[0].x)
#output_array=[]
logical_bookkeeping_array=[]
for current_pauli in all_pauli_bsvs(nq):
logical_bookkeeping_array = list(map(current_pauli.bsip, pauli_array_input))
if logical_bookkeeping_array == logical_array_input:
# output_array.append(current_pauli)
yield current_pauli
# return output_array
[docs]def commute(bsv1,bsv2):
"""
Returns True if bsv1 and bsv2 commute by evaluating the symplectic inner
product.
:rtype: bool
"""
return logical_not(logical_xor(bitwise_inner_product(bsv1.x,bsv2.z),bitwise_inner_product(bsv1.z,bsv2.x)))
[docs]def xz_switch(bsv):
"""
Given a :class:`qecc.BinarySymplecticVector`, returns a new vector whose
:math:`X` and :math:`Z` parts have been swapped.
"""
return BinarySymplecticVector(bsv.z,bsv.x)
## BINARY SYMPLECTIC MATRIX CLASS ##
[docs]class BinarySymplecticMatrix(object):
"""
Encapsulates a binary symplectic matrix representing an element of the Clifford
group on :math:`n` qubits.
A new :class:`BinarySymplecticMatrix` can be constructed using either a
single NumPy 2-D array containing the :math:`XX`, :math:`XZ`, :math:`ZX`, and :math:`ZZ`
parts of the binary symplectic matrix. Alternatively, a new matrix can be instantiated
using four NumPy arrays. For example, the following two invocations are
equivalent:
>>> import qecc
>>> import numpy as np
>>> bsm = qecc.BinarySymplecticMatrix(np.array([[1, 0, 0, 0],[1, 1, 0, 0],[0, 0, 1, 1],[0, 0, 0, 1]]))
>>> bsm = qecc.BinarySymplecticMatrix(np.array([[1, 0],[1, 1]]), np.array([[0, 0],[0, 0]]), np.array([[0, 0],[0, 0]]), np.array([[1, 1],[0, 1]]))
"""
def __init__(self,*args):
if len(args)==4:
self._arr=vstack((hstack((args[0],args[1])),hstack((args[2],args[3]))))
elif len(args)==1:
self._arr=args[0]
else:
raise ValueError("Either one or four arguments must be provided.")
## PROPERTIES ##
@property
def nq(self):
"""
Returns the number of qubits that the binary symplectic matrix acts
upon.
"""
return int(len(self._arr) / 2)
@property
def xx(self):
"""
Returns the upper-left quadrant of a binary symplectic matrix.
"""
nq = self.nq
return self._arr[0:nq,0:nq]
@property
def xz(self):
"""
Returns the upper-right quadrant of a binary symplectic matrix.
"""
nq = self.nq
return self._arr[0:nq,nq:2*nq]
@property
def zx(self):
"""
Returns the lower-left quadrant of a binary symplectic matrix.
"""
nq = self.nq
return self._arr[nq:2*nq,0:nq]
@property
def zz(self):
"""
Returns the lower-right quadrant of a binary symplectic matrix.
"""
nq = self.nq
return self._arr[nq:2*nq,nq:2*nq]
@property
def xc(self):
"""
Returns the left half of a binary symplectic matrix.
"""
nq = self.nq
return self._arr[:, 0:nq]
@xc.setter
def xc(self, newval):
self._arr[:, 0:self.nq] = newval
@property
def zc(self):
"""
Returns the right half of a binary symplectic matrix.
"""
nq = self.nq
return self._arr[:, nq:2*nq]
@zc.setter
def zc(self, newval):
self._arr[:, self.nq:2*self.nq] = newval
@property
def xr(self):
"""
Returns the top half of a binary symplectic matrix.
"""
nq = self.nq
return self._arr[0:nq, :]
@xr.setter
def xr(self, newval):
self._arr[0:self.nq, :] = newval
@property
def zr(self):
"""
Returns the bottom half of a binary symplectic matrix.
"""
nq = self.nq
return self._arr[nq:2*nq, :]
@zr.setter
def zr(self, newval):
self._arr[self.nq:2*self.nq, :] = newval
@xx.setter
def xx(self, thing):
nq = self.nq
self._arr[0:nq,0:nq]=thing
@xz.setter
def xz(self, thing):
nq = self.nq
self._arr[0:nq,nq:2*nq]=thing
@zx.setter
def zx(self, thing):
nq = self.nq
self._arr[nq:2*nq,0:nq]=thing
@zz.setter
def zz(self, thing):
nq = self.nq
self._arr[nq:2*nq,nq:2*nq]=thing
## MAGIC METHODS ##
def __getitem__(self, sliceobj):
return self._arr.__getitem__(sliceobj)
def __setitem__(self, sliceobj, val):
self._arr.__setitem__(sliceobj, val)
def __mul__(self,other):
return BinarySymplecticMatrix(dot(self._arr, other._arr)%2)
def __repr__(self):
# TODO: We could make this a bit
# fancier by putting lines between
# the blocks, but that'd take a
# silly amount of effort, so let's do
# that later.
return repr(self._arr)
def __eq__(self, other):
return all(self._arr == other._arr)
def __and__(self, other):
"""
Returns the direct sum of this binary symplectic
matrix with another matrix. Note that resulting
binary symplectic matrix represents the tensor
product of the two Clifford gates represented.
"""
attrs = ['xx', 'xz', 'zx', 'zz']
blocks = []
for att in attrs:
blocks.append(directsum(
getattr(self, att),
getattr(other, att)
))
return BinarySymplecticMatrix(*blocks)
## GATE METHODS ##
[docs] def left_H(self,j):
r"""
Multiplies on the left by a Hadamard gate on the :math:`j^{\text{th}}`
qubit. This method acts in-place, as opposed to acting on a copy of the
binary symplectic matrix. In order to preserve the original matrix,
use the :meth:`copy` method:
>>> new_bsm = bsm.copy().left_H(idx) # doctest: +SKIP
"""
u.array_swap(self.zr[j, :], self.xr[j, :])
return self
[docs] def right_H(self,j):
r"""
Multiplies on the right by a Hadamard gate on the :math:`j^{\text{th}}`
qubit. See :meth:`left_H` for more details.
"""
u.array_swap(self.zc[:, j], self.xc[:, j])
return self
[docs] def right_H_all(self):
r"""
Multiplies on the right by a Hadamard gate on each
qubit. See :meth:`left_H` for more details.
"""
u.array_swap(self.zc, self.xc)
return self
[docs] def left_SWAP(self,j,k):
r"""
Multiplies on the left by a SWAP gate between the :math:`j^{\text{th}}`
and :math:`k^{\text{th}}` qubits. This method acts in-place, as opposed
to acting on a copy of the binary symplectic matrix. In order to
preserve the original matrix, use the :meth:`copy` method:
>>> new_bsm = bsm.copy().left_SWAP(j, k) # doctest: +SKIP
"""
u.array_swap(self.xr[j, :], self.xr[k, :])
u.array_swap(self.zr[j, :], self.zr[k, :])
return self
[docs] def right_SWAP(self,j,k):
r"""
Multiplies on the right by a SWAP gate between the :math:`j^{\text{th}}`
and :math:`k^{\text{th}}` qubits. See :meth:`left_SWAP` for more
details.
"""
u.array_swap(self.xc[:, j], self.xc[:, k])
u.array_swap(self.zc[:, j], self.zc[:, k])
return self
[docs] def left_CNOT(self, c, t):
r"""
Multiplies on the left by a CNOT gate controlled by the
:math:`c^{\text{th}}` qubit and targeting the :math:`k^{\text{th}}`
qubit. This method acts in-place, as opposed to acting on a copy of the
binary symplectic matrix. In order to preserve the original matrix, use
the :meth:`copy` method:
>>> new_bsm = bsm.copy().left_CNOT(c, t) # doctest: +SKIP
"""
if c == t:
raise ValueError("Control and target qubits cannot be the same.")
self.xr[t, :] += self.xr[c, :]
self.xr[t, :] %= 2
self.zr[c, :] += self.zr[t, :]
self.zr[c, :] %= 2
return self
[docs] def right_CNOT(self, c, t):
r"""
Multiplies on the right by a CNOT gate controlled by the
:math:`c^{\text{th}}` qubit and targeting the :math:`k^{\text{th}}`
qubit. For more details, see :meth:`left_CNOT`.
"""
if c == t:
raise ValueError("Control and target qubits cannot be the same.")
self.xc[:, c] += self.xc[:, t]
self.xc[:, c] %= 2
self.zc[:, t] += self.zc[:, c]
self.zc[:, t] %= 2
return self
[docs] def left_R_pi4(self, i):
r"""
Multiplies on the left by an :math:`R_{\pi/4}` gate acting on the
:math:`i^{\text{th}}` qubit. This method acts in-place, as opposed to
acting on a copy of the binary symplectic matrix. In order to preserve
the original matrix, use the :meth:`copy` method:
>>> new_bsm = bsm.copy().left_R_pi4(c, t) # doctest: +SKIP
"""
self.zr[i, :] += self.xr[i, :]
self.zr[i, :] %= 2
return self
[docs] def right_R_pi4(self, i):
r"""
Multiplies on the right by an :math:`R_{\pi/4}` gate acting on the
:math:`i^{\text{th}}` qubit. For more details, see
:meth:`left_R_pi4`.
"""
self.xc[:, i] += self.zc[:, i]
self.xc[:, i] %= 2
return self
[docs] def left_CZ(self, c1, c2):
r"""
Multiplies on the left by an controlled-:math:`Z` gate acting between
the :math:`c_1^{\text{th}}` and :math:`c_2^{\text{th}}` qubits. This
method acts in-place, as opposed to acting on a copy of the binary
symplectic matrix. In order to preserve the original matrix, use the
:meth:`copy` method:
>>> new_bsm = bsm.copy().left_CZ(c, t) # doctest: +SKIP
"""
if c1 == c2:
raise ValueError("Control qubits cannot be the same.")
self.zr[c2, :] += self.xr[c1, :]
self.zr[c2, :] %= 2
self.zr[c1, :] += self.xr[c2, :]
self.zr[c1, :] %= 2
return self
[docs] def right_CZ(self, c1, c2):
r"""
Multiplies on the right by an controlled-:math:`Z` gate acting between
the :math:`c_1^{\text{th}}` and :math:`c_2^{\text{th}}` qubits. For more
details, see :meth:`left_CZ`.
"""
if c1 == c2:
raise ValueError("Control qubits cannot be the same.")
self.xc[:, c2] += self.zc[:, c1]
self.xc[:, c2] %= 2
self.xc[:, c1] += self.zc[:, c2]
self.xc[:, c1] %= 2
return self
## OTHER METHODS ##
[docs] def inv(self, check_validity=True):
"""
Returns the inverse of this binary symplectic matrix, assuming
that this matrix represents a valid Clifford gate.
Note that if the matrix :math:`H` does not represent a valid Clifford,
this method will return a matrix :math:`G` such that :math:`H G` is
not the identity matrix.
.. todo::
Cite that this inversion works due to the properties of symplectic
matrix groups.
:param bool check_validity: If ``True``, then the matrix is first
checked to ensure that it is a valid Clifford.
:raises: :class:`qecc.InvalidCliffordError` if ``check_validity`` is ``True``
and the binary symplectic matrix being inverted does not represent
a valid Clifford group element.
"""
if check_validity and not self.is_valid():
raise InvalidCliffordError('Matrix cannot be inverted.')
return BinarySymplecticMatrix(self.zz.T,self.xz.T,self.zx.T,self.xx.T)
[docs] def as_clifford(self, check_validity=True):
"""
Converts this binary symplectic matrix into a Clifford representation.
:param bool check_validity: If ``True``, then the matrix is first
checked to ensure that it is a valid Clifford.
:rtype: :class:`qecc.Clifford`
:returns: The same gate as this binary symplectic matrix, represented
as an instance of :class:`qecc.Clifford`.
"""
if check_validity and not self.is_valid():
raise InvalidCliffordError('Matrix cannot be converted.')
return Clifford(list(map(array_to_pauli,self.xc.T)),list(map(array_to_pauli,self.zc.T)))
[docs] def is_valid(self):
"""
Checks the satisfaction of the symplectic condition on a
:class:`qecc.BinarySymplecticMatrix` object.
"""
xrows = list(map(BinarySymplecticVector, self.xc.T))
zrows = list(map(BinarySymplecticVector, self.zc.T))
for idx_j in range(len(xrows)):
for idx_k in range(len(zrows)):
if xrows[idx_j].bsip(zrows[idx_k]) != (idx_j == idx_k):
return False
elif xrows[idx_j].bsip(xrows[idx_k]) != 0:
return False
elif zrows[idx_j].bsip(zrows[idx_k]) != 0:
return False
return True
[docs] def copy(self):
"""
Returns a copy of this binary symplectic matrix, pointing to a distinct
location in memory.
"""
return BinarySymplecticMatrix(self._arr.copy())
[docs] def circuit_decomposition(self, validate=True):
"""
Decomposes the binary symplectic matrix using the algorithm of [AG04]_.
.. todo::
Complete docstring.
"""
# The circuit decomposition algorithm is long enough that it was moved
# into another module, bsf_decomp.
left, right = bsf_decomp.circuit_decomposition_part1(self.copy())
circ = circuit.Circuit(*(right + list(reversed(left))))
if validate:
if all(self._arr == eye(self.nq * 2)):
assert circ.as_clifford() is EmptyClifford, "Decomposition of an empty circuit did not produce an empty Clifford."
else:
assert all(circ.as_clifford().as_bsm()._arr == self._arr), "Decomposition failed to produce desired BSM."
return circ
## BSM FUNCTIONS ##
[docs]@u.deprecated("Please use the is_valid() method of BinarySymplecticMatrix instead.")
def is_bsm_valid(input_bsm):
return input_bsm.is_valid()
[docs]def bsmzeros(nq):
"""
Returns a binary symplectic matrix on :math:`n` qubits, initialized to all
zeros.
:param int nq: Number of qubits that the created matrix will act upon.
:returns: A binary symplectic matrix containing all zeros.
:rtype: BinarySymplecticMatrix
"""
return BinarySymplecticMatrix(
zeros((2 * (nq),) * 2, dtype=int))
[docs]def array_to_pauli(bsv_array):
"""
Function wrapper for type conversion from binary symplectic vector
to :class:`qecc.Pauli`. See :meth:`qecc.BinarySymplecticVector.as_pauli`.
"""
return BinarySymplecticVector(bsv_array).as_pauli()
[docs]def directsum(A, B):
r"""
Given two matrices :math:`A` and :math:`B` with two indices each,
returns the direct sum :math:`A \oplus B`.
:type A: ndarray, shape (sA[0], sA[1])
:type B: ndarray, shape (sB[0], sB[1])
:rtype: ndarray, shape (sA[0] + sB[0], sA[1] + sB[1])
:returns: :math:`A \oplus B`
"""
sA = A.shape
sB = B.shape
return hstack([
vstack([A, zeros((sB[0], sA[1]))]),
vstack([zeros((sA[0], sB[1])), B])
])
## EXAMPLE USAGE ##
if __name__ == "__main__":
bsf_P = Pauli('XYZ').as_bsv()
print(bsf_P)
# Should print:
# ( 1 1 0 | 0 1 1 )
print(bsf_P.x.astype('int'))
# array([1, 1, 0])
bsf_cnot = cnot_gt(2, 0, 1).as_bsm()
print(bsf_cnot)
# ( 1 1 | 0 0
# 0 1 | 0 0
# ----+----
# 0 0 | 1 1
# 0 0 | 0 1 )
print(int(bsf_cnot.xx[0,1]))
# 1