#!/usr/bin/python
# -*- coding: utf-8 -*-
##
# PauliClass.py: Implementation of qecc.Pauli and related utility functions.
##
# © 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))
from itertools import product, chain, permutations, combinations, starmap
if PY3:
from itertools import filterfalse
else:
from itertools import ifilterfalse as filterfalse
from copy import copy
from operator import mul, and_, add
if PY3:
from . import bsf
from . import pred
from .circuit import Circuit, Location
from .paulicollections import PauliList
from .unitary_reps import pauli_as_unitary
from .singletons import Unspecified
from . import CliffordClass as cc
from . import utils as u
else:
import bsf
import pred
from circuit import Circuit, Location
from paulicollections import PauliList
from unitary_reps import pauli_as_unitary
from singletons import Unspecified
import CliffordClass as cc
import utils as u
from functools import reduce
## ALL ##
__all__ = [
'Pauli',
'ensure_pauli', 'com', 'pauli_group', 'from_generators',
'is_in_normalizer', 'elem_gen', 'elem_gens', 'eye_p', 'ns_mod_s',
'pad', 'mutually_commuting_sets',
'clifford_bottoms',
'paulis_by_weight','remove_phase','restricted_pauli_group','embed'
]
## CONSTANTS ##
VALID_OPS = ['I', 'X', 'Y', 'Z']
__all__ += VALID_OPS
VALID_PHS = list(range(4))
MULT_TABLE = {
('I', 'I'): (0, 'I'), ('I', 'X'): (0, 'X'), ('I', 'Y'): (0, 'Y'), ('I', 'Z'): (0, 'Z'),
('X', 'I'): (0, 'X'), ('X', 'X'): (0, 'I'), ('X', 'Y'): (1, 'Z'), ('X', 'Z'): (3, 'Y'),
('Y', 'I'): (0, 'Y'), ('Y', 'X'): (3, 'Z'), ('Y', 'Y'): (0, 'I'), ('Y', 'Z'): (1, 'X'),
('Z', 'I'): (0, 'Z'), ('Z', 'X'): (1, 'Y'), ('Z', 'Y'): (3, 'X'), ('Z', 'Z'): (0, 'I')
}
## CLASSES ##
[docs]class Pauli(object):
r"""
Class representing an element of the Pauli group on :math:`n` qubits.
:param operator: String of I's, X's, Y's and Z's.
:type operator: str
:param phase: A phase input as an integer from 0 to 3, interpreted as
:math:`i^{\mathrm{phase}}`.
:type phase: int
"""
def __init__(self, operator, phase=0):
# Operator is a string which contains I, X, Y, Z. We throw an error if it isn't.
for one_bit_operator in operator:
if one_bit_operator not in VALID_OPS:
raise ValueError("Input operators I, X, Y or Z.")
# Phase will be defined as an integer from 0 to 3. If it's not integral, we throw an error.
if not isinstance(phase, int):
raise ValueError("Input phase must be an integer, preferably 0 to 3.")
#If a phase outside of range(4) is input, we use its remainder, mod 4.
if not( phase > -1 and phase < 4):
phase= phase % 4
self.op = operator
self.ph = phase
def __hash__(self):
# We need a hash function to store Paulis as dict keys or in sets.
return hash((self.op, self.ph))
def __len__(self):
"""
Yields the number of qubits on which the Pauli ``self`` acts.
"""
return len(self.op)
def __mul__(self, other):
"""
Multiplies two Paulis, ``self`` and ``other`` symbolically, using tabulated single-Pauli multiplication.
"""
if not isinstance(other,Pauli):
return NotImplemented
p1 = self
p2 = other
if not(len(p1)==len(p2)):
raise ValueError("These Paulis are not the same length.")
#We initialize a Pauli with an empty op-string, updating the operator
#and phase using a multiplication table:
newP = Pauli('', p1.ph + p2.ph)
for paulicounter in range(len(p1)):
ph, op = MULT_TABLE[(p1.op[paulicounter], p2.op[paulicounter])]
newP.op=newP.op+op
newP.ph=newP.ph+ph
newP.ph = newP.ph % 4
return newP
def __pow__(self,num):
"""
Exponentiates a Pauli ``self`` by an integer ``num``, using the fact
that every element of the Pauli group squares to the identity.
"""
if not isinstance(num,int):
raise ValueError("Paulis can only be exponentiated with integers")
elif num % 2 == 0:
return eye_p(len(self))
elif num % 2 == 1:
return self
else:
raise ValueError("Unknown exponentiation error")
def __repr__(self):
"""
Representation for Paulis, printing Paulis in the format ``i^{phase} {op}``.
"""
return "i^{k} {op}".format(k=self.ph, op=self.op)
def __str__(self):
"""
Determines whether the Pauli ``self`` is sparse (acting as the identity
on a majority of qubits as determined by :param SPARSE_THRESH:), and
returns a string in one of two formats, depending on sparsity.
For example:
>>> import qecc as q
>>> print q.Pauli('IXIZIII')
i^0 X[1] Z[3]
>>> import qecc as q
>>> print q.Pauli('IXIZIIX')
i^0 IXIZIIX
Note that Paulis which do not act on more than :param SPARSE_NQ: qubits
are never printed in the sparse format:
>>> import qecc as q
>>> print q.Pauli('IXII')
i^0 IXII
"""
SPARSE_NQ = 5
SPARSE_THRESH = 0.3
if len(self) <= SPARSE_NQ or self.wt/len(self) > SPARSE_THRESH:
return repr(self)
else:
return self.str_sparse()
def __getitem__(self, idxs):
"""
Returns the representation of a Pauli ``self`` on a single qubit
``idxs``.
:param idxs: Support of returned Pauli.
:rtype: :class:`qecc.Pauli`
"""
return Pauli(self.op[idxs], phase=self.ph)
## PROPERTIES ##
@property
def nq(self):
"""
Returns the number of qubits upon which this Pauli operator acts.
"""
return len(self)
@property
def wt(self):
"""
Measures the weight of a given Pauli.
:rtype: int (between 0 and the number of qubits on which the Pauli is defined)
:returns: The number of qubits on which the represented Pauli operator
is supported.
"""
return len([op for op in self.op if op != 'I'])
## PRINTING ##
[docs] def str_sparse(self, incl_ph=True):
"""
Returns a compact representation for :class:`qecc.Pauli` objects, for
those having support on a small number of qubits of a large register.
"""
return ("i^{} ".format(self.ph) if incl_ph else "") + (" ".join(
"{}[{}]".format(P, idx) for idx, P in enumerate(self.op) if P != "I"
) if self.wt > 0 else "I")
## ALGEBRAIC OPERATORS ##
def __neg__(self):
"""
Negates (multiplying by :math:`-1`) a Pauli.
"""
return copy(self).mul_phase(2)
[docs] def tens(self, other):
r"""
Concatenates the op strings of two Paulis, and multiplies their phases, to produce
the Kronecker product of the two.
:param other: Pauli operator :math:`Q` to be tensored with this instance.
:type other: qecc.Pauli
:returns: An instance representing :math:`P\otimes Q`, where :math:`P`
is the Pauli operator represented by this instance.
"""
return Pauli(self.op + other.op, self.ph + other.ph)
def __and__(self,other):
"""
Method implementing tensor products for :class:`qecc.Pauli`
objects; see `tens`.
"""
if not isinstance(other, Pauli):
return NotImplemented
return self.tens(other)
## MUTATOR METHODS ##
[docs] def set_phase(self, ph=0):
"""
Returns a :class:`qecc.Pauli` object having the same operator as
the input, with a specified phase (usually used to erase phases).
"""
self.ph = ph
return self
[docs] def mul_phase(self, ph):
r"""
Increments the phase of this Pauli by :math:`i^{\mathrm{ph}}`.
:param ph: Amount the phase is to be incremented by.
:type ph: int
:returns: This instance.
"""
self.ph = (self.ph + ph) % 4
return self
[docs] def permute_op(self, perm):
r"""
Returns a new :class:`qecc.Pauli` instance whose operator part
is related to the operator part of this Pauli, so that
:math:`\sigma_{\mu}` is mapped to :math:`\sigma_{\pi(\mu)}` for some
permutation :math:`\pi` of the objects :math:`{X, Y, Z}`.
For example:
>>> import qecc as q
>>> P = q.Pauli('XYZXYZ')
>>> print P.permute_op('ZXY')
i^0 ZXYZXY
Note that the result is **not** guaranteed to be the result of a
Clifford operator acting on this Pauli, as permutation may not respect
the phases introduced by taking products. For example:
>>> import qecc as q
>>> P = q.Pauli('XYZ')
>>> Q = q.Pauli('YYZ')
>>> P * Q
i^1 ZII
>>> Pp = P.permute_op('ZYX')
>>> Qp = Q.permute_op('ZYX')
>>> Pp * Qp
i^3 XII
:param list perm: A list indicating which permutation is to be
performed.
:return: A new instance `Q` of :class:`qecc.Pauli` that is related to
this instance by a permutation of :math:`X`, :math:`Y` and
:math:`Z`.
"""
new_operator=''
for letter in self.op:
if letter == 'X':
new_operator += perm[0]
elif letter == 'Y':
new_operator += perm[1]
elif letter == 'Z':
new_operator += perm[2]
elif letter == 'I':
new_operator=new_operator+'I'
return Pauli(new_operator, phase=self.ph)
## EQUALITY METHODS ##
def __eq__(self,other):
"""
Tests if two input Paulis, :obj:`self` and :obj:`other`, are equal.
:rtype: bool
"""
#First, make sure both objects are Paulis:
if isinstance(other, Pauli):
if self.op == other.op and self.ph == other.ph:
return True
else:
return False
else:
return False
def __ne__(self, other):
"""
Tests if two input Paulis, :obj:`self` and :obj:`other`, are not equal.
:rtype: bool
"""
#Check types
if isinstance(other, Pauli):
if self.op == other.op and self.ph == other.ph:
return False
else:
return True
else:
return True
## OTHER MAGIC METHODS ##
def __len__(self):
"""
Yields the number of qubits on which the Pauli ``self`` acts.
:rtype: int
"""
return len(self.op)
## CONVERSION METHODS ##
[docs] def as_gens(self):
"""
Expresses an input Pauli in terms of the elementary generators :math:`X_j` and :math:`Z_j`, stripping off phases.
:rtype: list of :class:`qecc.Pauli` instances.
"""
nq=len(self)
#Initialize generator list to an empty list:
pauli_gens=[]
for idx in range(nq):
#Since phases are being stripped, Y = XZ:
if self.op[idx]=='Y':
pauli_gens.append(elem_gen(nq,idx,'X'))
pauli_gens.append(elem_gen(nq,idx,'Z'))
elif self.op[idx]=='X':
pauli_gens.append(elem_gen(nq,idx,'X'))
elif self.op[idx]=='Z':
pauli_gens.append(elem_gen(nq,idx,'Z'))
return pauli_gens
[docs] def as_bsv(self):
"""
Converts the given Pauli to a binary symplectic vector, discarding phase
information.
:return: A binary symplectic vector representing this Pauli operator.
:rtype: BinarySymplecticVector
"""
nq = len(self)
exes=''
zeds=''
for idx_q in range(nq):
if self.op[idx_q]=='X':
exes=exes+'1'
zeds=zeds+'0'
elif self.op[idx_q]=='Y':
exes=exes+'1'
zeds=zeds+'1'
elif self.op[idx_q]=='Z':
exes=exes+'0'
zeds=zeds+'1'
elif self.op[idx_q]=='I':
exes=exes+'0'
zeds=zeds+'0'
return bsf.BinarySymplecticVector(exes,zeds)
[docs] def as_circuit(self):
""" Transforms an n-qubit Pauli to a serial circuit on
n qubits. Neglects global phases.
:rtype: :class:`qecc.Circuit`
"""
gatelist=[]
for idx, letter in enumerate(self.op):
if letter != "I":
gatelist.append((letter,idx))
return Circuit(*gatelist)
[docs] def as_unitary(self):
"""
Returns a :class:`numpy.ndarray` containing a unitary matrix
representation of this Pauli operator.
Raises a :obj:`RuntimeError` if NumPy cannot be imported.
"""
return pauli_as_unitary(self)
[docs] def as_clifford(self):
"""
Converts a Pauli into a Clifford which changes the signs of input
Paulis.
:returns: A Clifford representing conjugation by this Pauli
operator.
:rtype: :class:`qecc.Clifford`
"""
Xs, Zs = elem_gens(len(self))
for P in chain(Xs, Zs):
if com(self, P) == 1:
P.mul_phase(2)
return cc.Clifford(Xs, Zs)
[docs] @staticmethod
def from_sparse(sparse_pauli, nq=None):
"""
Given a dictionary from non-negative integers to single-qubit Pauli
operators or strings representing single-qubit Pauli operators, creates
a new instance of :class:`qecc.Pauli` representing the input.
>>> from qecc import Pauli, X, Y, Z
>>> print Pauli.from_sparse({3: X, 5: X, 7: Z}, nq=12)
i^0 X[3] X[5] Z[7]
:param dict sparse_pauli: Dictionary from qubit indices (non-negative
integers) to single-qubit Pauli operators or to strings.
:param int nq: If not ``None``, specifies the number of qubits on which
the newly created Pauli operator is to act.
"""
if nq is None:
nq = max(sparse_pauli.keys())
return reduce(and_, (
ensure_pauli(sparse_pauli[idx]) if idx in sparse_pauli else I
for idx in range(nq)
))
[docs] @staticmethod
def from_clifford(cliff_in):
"""
Tests an input Clifford ``cliff_in`` to determine if it is, in
fact, a Pauli. If so, it outputs the Pauli. If not, it
raises an error.
:arg cliff_in: Representation of Clifford operator to be converted,
if possible.
:rtype: :class:`qecc.Pauli`
Example:
>>> import qecc as q
>>> cliff = q.Clifford([q.Pauli('XI',2),q.Pauli('IX')], map(q.Pauli,['ZI','IZ']))
>>> q.Pauli.from_clifford(cliff)
i^0 ZI
Converting a Pauli into a Clifford and back again will erase
the phase:
>>> import qecc as q
>>> paul = q.Pauli('YZ',3)
>>> cliff = paul.as_clifford()
>>> q.Pauli.from_clifford(cliff)
i^0 YZ
"""
nq=len(cliff_in.xout) #Determine number of qubits.
test_ex,test_zed=elem_gens(nq) #Get paulis to compare.
"""If the Paulis input to the Clifford are only altered in phase, then the Clifford is also a Pauli."""
for ex_clif,zed_clif,ex_test,zed_test in zip(cliff_in.xout, cliff_in.zout,test_ex,test_zed):
if ex_clif.op != ex_test.op or zed_clif.op != zed_test.op:
raise ValueError("Clifford is not Pauli.")
#If the Clifford is Pauli, determine which by examining operators with altered phases.
exact=eye_p(nq)
zedact=eye_p(nq) #Initialize accumulators
"""If a negative sign appears on a given generator, assign a Pauli to that qubit that conjugates the generator to a minus sign, e.g. ZXZ = -X """
for idx_x in range(nq):
if cliff_in.xout[idx_x].ph==2:
exact.op = cc.replace_one_character(exact.op, idx_x, 'Z')
for idx_z in range(nq):
if cliff_in.zout[idx_z].ph==2:
zedact.op = cc.replace_one_character(zedact.op, idx_z, 'X')
return Pauli((exact*zedact).op)
[docs] @staticmethod
def from_string(bitstring, p_1):
"""
Creates a multi-qubit Pauli using a bitstring and a one-qubit Pauli, by
replacing all instances of 1 in the bitstring with the appropriate
Pauli, and replacing all instances of 0 with the identity.
:param list bitstring: a list of integers, each of which is either 0 or
1. `bitstring` can also be a string, type conversion is automatic.
:param str p_1: a single-qubit Pauli.
:returns: a phaseless Pauli from a bitstring. The intended use of
this function is as a quick means of specifying binary Paulis.
p_1 is the one_qubit Pauli that a '1' represents.
:rtype: :class:`qecc.Pauli`
Example:
>>> import qecc as q
>>> bitstring = '101110111100'
>>> p_1 = q.Pauli('X')
>>> q.Pauli.from_string(bitstring, p_1)
i^0 XIXXXIXXXXII
"""
p_1 = ensure_pauli(p_1)
if isinstance(bitstring, str):
bitstring = list(map(int, bitstring))
return reduce(and_,[(p_1.set_phase())**j for j in bitstring])
## OTHER METHODS ##
[docs] @u.deprecated("Use slice indexing and the wt property. Ex: P[0:2].wt")
def reg_wt(self, region):
"""
Produces the number of qubits within a subset of the register on which
the Pauli in question acts non-trivially.
:param tuple region: a tuple containing the indices on which the weight
is to be evaluated.
:returns: the number of qubits in the sub-register on which the Pauli
``self`` does not act as the identity.
"""
wt=0
for idx in range(len(self)):
if idx in region and self.op[idx]!='I':
wt += 1
return wt
[docs] def cust_wt(self,char):
"""
Produces the number of qubits on which an input Pauli acts as a
specified single-qubit Pauli.
:param str char: a single-letter string containing an I, X, Y or Z.
:returns: the number of qubits in the Pauli ``self`` which are acted upon
by the single-qubit operator ``char``.
"""
if char not in VALID_OPS:
raise ValueError('Generators cannot be selected outside I, X, Y, Z.')
return len([op for op in self.op if op == char])
[docs] def ct(self):
"""
The conjugate transpose of this Pauli operator.
:rtype: an instance of the :class:`qecc.Pauli` class.
"""
return Pauli(self.op, 4-self.ph)
[docs] def centralizer_gens(self, group_gens=None):
r"""
Returns the generators of the centralizer group :math:`\mathrm{C}(P)`,
where :math:`P` is the Pauli operator represented by this instance.
If ``group_gens`` is specified, :math:`\mathrm{C}(P)` is taken to be
a subgroup of the group :math:`G = \langle G_1, \dots, G_k\rangle`,
where :math:`G_i` is the :math:`i^{\text{th}}` element of
``group_gens``.
:param group_gens: Either ``None`` or a list of generators :math:`G_i`.
If not ``None``, the returned centralizer :math:`\mathrm{C}(P)` is
a subgroup of the group :math:`\langle G_i \rangle_{i=1}^k`.
:type group_gens: list of :class:`qecc.Pauli` instances
:returns: A list of elements :math:`P_i` of the Pauli group such that
:math:`\mathrm{C}(P) = \langle P_i \rangle_{i=1}^{n}`, where
:math:`n` is the number of unique generators of the centralizer.
"""
if group_gens is None:
Xs, Zs = elem_gens(len(self))
group_gens = Xs + Zs
if not group_gens: # If group_gens is empty, it's false-y.
return PauliList()
if com(self, group_gens[0]) == 0:
# That generator commutes, and so we pass it along
# unmodified.
return PauliList(group_gens[0], *self.centralizer_gens(group_gens[1:]))
else:
# That generator anticommutes, and so we must modify it by
# multiplication with another anticommuting generator, if one
# exists.
found = False
for idx in range(1, len(group_gens)):
if com(self, group_gens[idx]) == 1:
found = True
g_prime = group_gens[idx] * group_gens[0]
assert com(self, g_prime) == 0
return PauliList(g_prime, *self.centralizer_gens(group_gens[1:]))
if not found:
# Generator 0 anticommuted, and so we know two things
# from our search:
# - All other generators commute.
# - The anticommuting generator (0) has no match, and must
# be excluded.
return PauliList(*group_gens[1:])
## COMPARISON METHODS ##
[docs] def hamming_dist(self, other):
r"""
Returns the Hamming distance between this and another Pauli operator,
defined as :math:`d(P, Q) = \mathrm{wt}(PQ)`.
"""
return (self * other).wt
## MORE CONSTANTS ##
I = Pauli('I')
X = Pauli('X')
Y = Pauli('Y')
Z = Pauli('Z')
## FUNCTIONS ##
def ensure_pauli(P):
"""
Given either a string or an instance of :class:`qecc.Pauli`, produces an
instance of :class:`qecc.Pauli`.
"""
return P if isinstance(P, Pauli) or P is Unspecified else Pauli(P)
# Stolen from itertools cookbook.
def powerset(iterable):
"""
Sub-function, stolen from itertools cookbook.
powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)
"""
s = list(iterable)
return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))
def paulis_by_weight(nq, wt):
"""
Produces an iterator including all Pauli operators on ``nq`` qubits
of weight ``wt``.
:param int nq: The number of qubits on which the returned Paulis are to act.
:param int wt: The weight of each returned Pauli.
"""
def error_by_idxs(idxs, err_string):
return reduce(mul, starmap(elem_gen, list(zip([nq]*len(idxs), idxs, err_string))))
if wt == 0:
return iter([eye_p(nq)])
elif wt == 1:
return iter(elem_gen(nq, idx, kind) for kind in ['X', 'Y', 'Z'] for idx in range(nq))
else:
return chain(
paulis_by_weight(nq, wt - 1),
(error_by_idxs(idxs, err_string) for err_string in product('XYZ', repeat=wt) for idxs in combinations(list(range(nq)), wt))
)
def restricted_pauli_group(qubits_tpl,nq):
"""
Outputs an iterator onto the Pauli group on the qubits specified in
qubits_tpl, given the total number of qubits nq.
"""
return map(lambda pauli: embed(pauli,qubits_tpl,nq), pauli_group(len(qubits_tpl)))
def embed(pauli,qubits_tpl,nq):
"""
Takes a Pauli, defined on as many qubits as are in qubits_tpl, and acts it
on the register specified by qubits_tpl, within a register nq long.
"""
new_pauli_op='I'*nq
new_pauli_ph=pauli.ph
for idx in range(len(qubits_tpl)):
new_pauli_op = cc.replace_one_character(new_pauli_op, qubits_tpl[idx], pauli.op[idx])
return Pauli(new_pauli_op,new_pauli_ph)
[docs]def com(P, Q):
r"""
Given two elements *P* and *Q* of a Pauli group, returns 0 if :math:`[P, Q] = 0`
and returns 1 if :math:`\{P, Q\} = 0`.
:param P: Representation of :math:`P`.
:type P: qecc.Pauli
:param Q: Representation of :math:`Q`.
:type Q: qecc.Pauli
:returns: :math:`c(P, Q)`.
:rtype: :obj:`int`
"""
ph1, ph2 = (P*Q).ph, (Q*P).ph
return 0 if ph1 == ph2 else 1
[docs]def pauli_group(nq):
"""
Generates an iterator onto the Pauli group of :math:`n` qubits,
where :math:`n` is given as the argument *nq*.
:param nq: The number of qubits acted upon by the returned Pauli group.
:type nq: int
:returns: An iterator such that ``list(pauli_group(nq))`` produces a list of
all possible Pauli operators on ``nq`` qubits.
"""
for op in product(VALID_OPS, repeat=nq):
yield Pauli("".join(op), 0)
[docs]def from_generators(gens, coset_rep=None, incl_identity=True):
"""
Given a list of generators ``gens``, yields an iterator
onto the group generated by products of elements from
``gens``.
If ``coset_rep`` is specified, returns the coset of the group generated by
``gens`` represented by ``coset_rep``.
"""
if coset_rep is None:
coset_rep = eye_p(len(gens[0]))
for prod in powerset(gens):
if len(prod)>0:
yield reduce(lambda P, Q: P*Q, prod, coset_rep)
elif incl_identity:
yield coset_rep
# Ignore this element of the powerset if incl_identity is False.
[docs]def is_in_normalizer(pauli, stab):
"""
Given an element ``pauli`` of a Pauli group and the generators
``stab`` of a stabilizer group :math:`S`, returns True if and only if
``pauli`` is in the normalizer :math:`N(S)`.
"""
stab_group = from_generators(stab)
com_vec = [com(pauli, stab_elem) for stab_elem in stab_group]
return all([x == 0 for x in com_vec])
def elem_gen(nq, q, op):
"""
Produces a weight-one Pauli on a specific qubit ``q`` out of ``nq``.
:param int nq: Number of qubits upon which the returned Pauli acts.
:param int q: Index of the qubit upon which the returned Pauli acts
non-trivially.
:param str op: Either ``"I"``, ``"X"``, ``"Y"`` or ``"Z"``, indicating which
generator to return.
:returns: An ``nq``-qubit operator that acts as ``op`` on qubit ``q``, and
that acts as identity on all other qubits.
:rtype: qecc.Pauli
"""
if op in VALID_OPS:
return Pauli('I'*q + op + 'I'*(nq-q-1))
else:
raise ValueError('Generators cannot be selected outside I, X, Y, Z.')
[docs]def elem_gens(nq):
"""
Produces all weight-one :math:`X` and :math:`Z` operators on `nq` qubits.
For example,
>>> import qecc as q
>>> Xgens, Zgens = q.elem_gens(2)
>>> print Xgens[1]
i^0 IX
:param int nq: Number of qubits for each returned operator.
:returns: a tuple of two lists, containing :math:`X` and :math:`Z`
generators, respectively.
"""
return tuple(PauliList(*[elem_gen(nq,idx,P) for idx in range(nq)]) for P in ['X','Z'])
[docs]def eye_p(nq):
"""
Given a number of qubits, returns the identity Pauli on that many qubits.
:param int nq: Number of qubits upon which the returned Pauli acts.
:rtype: :class:`qecc.Pauli`
:returns: A Pauli operator acting as the identity on each of ``nq`` qubits.
"""
return Pauli('I'*nq)
def ns_mod_s(*stab_gens):
r"""
Given the generators of a stabilizer group :math:`S`, returns an iterator
that yields all elements of the set :math:`\text{N}(S)\\S`.
:param qecc.Pauli stab_gens: Generators of :math:`S`.
:returns: An iterator such that ``list(ns_mod_s(*stab_gens))`` contains
each element of :math:`\text{N}(S)\\S`.
"""
nq = len(stab_gens[0])
return filter(
pred.commutes_with(*stab_gens) & ~pred.in_group_generated_by(*stab_gens),
pauli_group(nq)
)
[docs]def mutually_commuting_sets(n_elems, n_bits=None, group_gens=None, exclude=None):
r"""
Yields an iterator onto tuples representing mutually commuting sets of
``n_elems`` independent Pauli operators, excluding the identity Pauli.
:param int n_elems: The number of mutually commuting Pauli operators to
include in each tuple.
:param int n_bits: The number of qubits on which each Pauli operator
considered by this iterator acts. If ``None``, defaults to the number of
qubits on which the first element of ``group_gens`` acts.
:param group_gens: The generators of the group in which to search for
mutually commuting Pauli operators. Defaults to the elementary
generators of the Pauli group on ``n_bits`` qubits.
:type group_gens: ``None`` or a sequence of :class:`qecc.Pauli` instances
:param exclude: If not ``None``, the iterator will omit from its search any
operators in the group generated by ``exclude``.
:type exclude: ``None`` or a sequence of :class:`qecc.Pauli` instances
"""
if n_elems == 0:
yield ()
return
if exclude is None:
if n_bits is not None:
exclude = [eye_p(n_bits)]
else:
if group_gens is not None and len(group_gens) > 0:
exclude = [eye_p(len(group_gens[0]))]
assert len(exclude) > 0
if group_gens is None and n_bits is None:
raise ValueError('Either a generating set or a number of qubits must be specified.')
if group_gens is None:
group_gens = add(*elem_gens(n_bits))
assert len(group_gens) > 0
for P in filterfalse(lambda q: q.wt==0 or q in from_generators(exclude),from_generators(group_gens)):
P = Pauli(P.op, phase=0)
if n_elems==1:
yield (P,)
else:
c_gens=P.centralizer_gens(group_gens)
for S in mutually_commuting_sets(n_elems-1,group_gens=c_gens, exclude=exclude+[P]):
yield (P,)+S
@u.deprecated("Deprecated; see PauliList.pad")
def pad(setP, n_eb=0, lower_right=None):
r"""
Takes a set of :class:`qecc.Pauli` objects, and returns a new set,
appending ``n_eb`` qubits, with stabilizer operators specified by
``lower_right``.
:arg setP: list of Pauli operators to be padded.
:param int n_eb: Number of extra bits to be appended to the system.
:param lower_right: list of `qecc.Pauli` operators, acting on `n_eb` qubits.
:rtype: list of :class:`qecc.Pauli` objects.
Example:
>>> import qecc as q
>>> pauli_set=map(q.Pauli,['XXX','YIY','ZZI'])
>>> q.pad(pauli_set,n_eb=2,lower_right=map(q.Pauli,['IX','ZI']))
[i^0 XXXII, i^0 YIYII, i^0 ZZIII, i^0 IIIIX, i^0 IIIZI]
"""
# Ensure that we have lists, and make a copy.
setP= list(setP)
len_P=len(setP)
n_P=len(setP[0])
if n_eb == 0 and lower_right is None:# or len(lower_right)==0:
return setP
elif len(lower_right) != 0:
n_eb=len(lower_right[0])
setout=[]
for pauli in setP:
setout.append(Pauli(pauli.op+'I'*n_eb))
if lower_right is None:
for jj in n_eb:
setout.append(eye_p(n_P+n_eb))
else:
for opmo in lower_right:
setout.append(eye_p(n_P)&opmo)
return setout
def clifford_bottoms(c_top):
r"""
This function yields the next set of nq paulis that mutually
commute, and anti-commute with selected elements from c_top. The intent
behind this function is to begin with a list of mutually commuting Paulis,
and ifnd an associated set that share the same commutation relations as
:math:`\left[ X_1,\ldots,X_n \range]` and
:math:`\left[ Z_1,\ldots,Z_n \range]`. This allows the input and output of
this function can be thought of as the :math:`X` and :math:`Z` outputs of a
Clifford operator.
:param c_top: list of :class:`qecc.Pauli` objects, which mutually commute.
"""
nq=len(c_top)
possible_zs=[]
for jj in range(nq):
applicable_centralizer_gens=PauliList(*(c_top[:jj]+c_top[jj+1:])).centralizer_gens()
possible_zs.append([a for a in from_generators(applicable_centralizer_gens) if com(a,c_top[jj])==1])
for possible_set in product(*possible_zs):
if all(map(lambda twolist: pred.commutes_with(twolist[0])(twolist[1]),combinations(possible_set,2))):
yield possible_set
def remove_phase(pauli):
return Pauli(pauli.op)
## MAIN ##
if __name__ == "__main__":
P = Pauli('XYZ', 0)
Q = Pauli('YXI', 0)
R = Pauli('XXI', 0)
print(list(pauli_group(2)))
print(list(from_generators([P, Q, R])))
print(is_in_normalizer(Pauli('ZZX', 0), [P, Q, R]))