def block_logical_pauli(self, P): r""" Given a Pauli operator :math:`P` acting on :math:`k`, finds a Pauli operator :math:`\overline{P}` on :math:`n_k` qubits that corresponds to the logical operator acting across :math:`k` blocks of this code. Note that this method is only supported for single logical qubit codes. """ if self.nq_logical > 1: raise NotImplementedError("Mapping of logical Pauli operators is currently only supported for single-qubit codes.") # TODO: test that phases are handled correctly. # FIXME: cache this dictionary. replace_dict = { 'I': p.eye_p(self.nq), 'X': self.logical_xs[0], 'Y': (self.logical_xs[0] * self.logical_zs[0]).mul_phase(1), 'Z': self.logical_zs[0] } # FIXME: using eye_p(0) is a hack. return reduce(op.and_, (replace_dict[sq_op] for sq_op in P.op), p.eye_p(0))
def flip_code(n_correctable, stab_kind='Z'): """ Creates an instance of :class:`qecc.StabilizerCode` representing a code that protects against weight-``n_correctable`` flip errors of a single kind. This method generalizes the bit-flip and phase-flip codes, corresponding to ``stab_kind=qecc.Z`` and ``stab_kind=qecc.X``, respectively. :param int n_correctable: Maximum weight of the errors that can be corrected by this code. :param qecc.Pauli stab_kind: Single-qubit Pauli operator specifying which kind of operators to use for the new stabilizer code. :rtype: qecc.StabilizerCode """ nq = 2 * n_correctable + 1 stab_kind = p.ensure_pauli(stab_kind) if len(stab_kind) != 1: raise ValueError("stab_kind must be single-qubit.") return StabilizerCode( [p.eye_p(j) & stab_kind & stab_kind & p.eye_p(nq-j-2) for j in range(nq-1)], ['X'*nq], ['Z'*nq], label='{}-flip code (t = {})'.format(stab_kind.op, n_correctable) )
def transcoding_cliffords(self,other): r""" Returns an iterator onto all :class:`qecc.Clifford` objects which take states specified by ``self``, and return states specified by ``other``. :arg other: :class:`qecc.StabilizerCode` """ #Preliminaries: stab_in = self.group_generators stab_out = other.group_generators xs_in = self.logical_xs xs_out = other.logical_xs zs_in = self.logical_zs zs_out = other.logical_zs nq_in=len(stab_in[0]) nq_out=len(stab_out[0]) nq_anc=abs(nq_in-nq_out) #Decide left side: if nq_in<nq_out: stab_left=stab_out xs_left=xs_out zs_left=zs_out stab_right=stab_in xs_right=xs_in zs_right=zs_in else: stab_right=stab_out xs_right=xs_out zs_right=zs_out stab_left=stab_in xs_left=xs_in zs_left=zs_in cliff_xouts_left=stab_left+xs_left cliff_zouts_left=[Unspecified]*len(stab_left)+zs_left cliff_left=c.Clifford(cliff_xouts_left,cliff_zouts_left).constraint_completions().next() list_left=cliff_left.xout+cliff_left.zout for mcset in p.mutually_commuting_sets(n_elems=len(stab_left)-len(stab_right),n_bits=nq_anc): temp_xouts_right = p.pad(stab_right,lower_right=mcset) + map(lambda elem: elem & p.eye_p(nq_anc), xs_right) temp_zouts_right = [Unspecified]*len(stab_left) + map(lambda elem: elem & p.eye_p(nq_anc), zs_right) for completion in c.Clifford(temp_xouts_right,temp_zouts_right).constraint_completions(): if nq_in < nq_out: yield c.gen_cliff(completion.xout+completion.zout,list_left) else: yield c.gen_cliff(list_left,completion.xout+completion.zout)
def min_len_transcoding_clifford(self,other): """ Searches the iterator provided by `transcoding_cliffords` for the shortest circuit decomposition. """ circuit_iter=map(lambda p: p.as_bsm().circuit_decomposition(), self.transcoding_cliffords(other)) return min(*circuit_iter)
def in_group_generated_by(*paulis): """ Returns a predicate that selects Pauli operators in the group generated by a given list of generators. """ # Warning: This is inefficient for large groups! paulis = list(map(pc.ensure_pauli, paulis)) return PauliMembershipPredicate(pc.from_generators(paulis), ignore_phase=True)
def concatenate(self,other): r""" Returns the stabilizer for a concatenated code, given the stabilizers for two codes. At this point, it only works for two :math:`k=1` codes. """ if self.nq_logical > 1 or other.nq_logical > 1: raise NotImplementedError("Concatenation is currently only supported for single-qubit codes.") nq_self = self.nq nq_other = other.nq nq_new = nq_self * nq_other # To obtain the new generators, we must apply the stabilizer generators # to each block of the inner code (self), as well as the stabilizer # generators of the outer code (other), using the inner logical Paulis # for the outer stabilizer generators. # Making the stabilizer generators from the inner (L0) code is straight- # forward: we repeat the code other.nq times, once on each block of the # outer code. We use that PauliList supports tensor products. new_generators = sum( ( p.eye_p(nq_self * k) & self.group_generators & p.eye_p(nq_self * (nq_other - k - 1)) for k in range(nq_other) ), pc.PauliList()) # Each of the stabilizer generators due to the outer (L1) code can be # found by computing the block-logical operator across multiple L0 # blocks, as implemented by StabilizerCode.block_logical_pauli. new_generators += map(self.block_logical_pauli, other.group_generators) # In the same way, the logical operators are also found by mapping L1 # operators onto L0 qubits. # This completes the definition of the concatenated code, and so we are # done. return StabilizerCode(new_generators, logical_xs=map(self.block_logical_pauli, other.logical_xs), logical_zs=map(self.block_logical_pauli, other.logical_zs) )
def ancilla_register(nq=1): r""" Creates an instance of :class:`qecc.StabilizerCode` representing an ancilla register of ``nq`` qubits, initialized in the state :math:`\left|0\right\rangle^{\otimes \text{nq}}`. :rtype: qecc.StabilizerCode """ return StabilizerCode( p.elem_gens(nq)[1], [], [] )
def unencoded_state(nq_logical=1, nq_ancilla=0): """ Creates an instance of :class:`qecc.StabilizerCode` representing an unencoded register of ``nq_logical`` qubits tensored with an ancilla register of ``nq_ancilla`` qubits. :param int nq_logical: Number of qubits to :rtype: qecc.StabilizerCode """ return ( StabilizerCode([], *p.elem_gens(nq_logical)) & StabilizerCode.ancilla_register(nq_ancilla) )
def possible_faults(circuit): """ Takes a sub-circuit which has been padded with waits, and returns an iterator onto Paulis which may occur as faults after this sub-circuit. :param qecc.Circuit circuit: Subcircuit to in which faults are to be considered. """ return it.chain.from_iterable( pc.restricted_pauli_group(loc.qubits, circuit.nq) for loc in circuit )
def logical_pauli_group(self, incl_identity=True): r""" Iterator onto the group :math:`\text{N}(S) / S`, where :math:`S` is the stabilizer group describing this code. Each member of the group is specified by a coset representative drawn from the respective elements of :math:`\text{N}(S) / S`. These representatives are chosen to be the logical :math:`X` and :math:`Z` operators specified as properties of this instance. :param bool incl_identity: If ``False``, the identity coset :math:`S` is excluded from this iterator. :yields: A representative for each element of :math:`\text{N}(S) / S`. """ return p.from_generators(self.logical_xs + self.logical_zs, incl_identity=incl_identity)
def minimize_distance_from(self, other, quiet=True): """ Reorders the stabilizer group generators of this code to minimize the Hamming distance with the group generators of another code, using a greedy heuristic algorithm. """ self_gens = self.group_generators other_gens = other.group_generators for idx_generator in range(len(self_gens)): min_hdist = self.nq + 1 # Effectively infinite. min_wt = self.nq + 1 best_gen = None best_gen_decomp = () for stab_elems in p.powerset(self_gens[idx_generator:]): if len(stab_elems) > 0: stab_elem = reduce(op.mul, stab_elems) hd = stab_elem.hamming_dist(other_gens[idx_generator]) if hd <= min_hdist and stab_elem.wt <= min_wt and (hd < min_hdist or stab_elem.wt < min_wt): min_hdist = hd min_wt = stab_elem.wt best_gen = stab_elem best_gen_decomp = stab_elems assert best_gen is not None, "Powerset iteration failed." if best_gen in self_gens: # Swap so that it lies at the front. idx = self_gens.index(best_gen) if not quiet and idx != idx_generator: print 'Swap move: {} <-> {}'.format(idx_generator, idx) self_gens[idx_generator], self_gens[idx] = self_gens[idx], self_gens[idx_generator] else: # Set the head element to best_gen, correcting the rest # as needed. if self_gens[idx_generator] in best_gen_decomp: if not quiet: print 'Set move: {} = {}'.format(idx_generator, best_gen) self_gens[idx_generator] = best_gen else: if not quiet: print 'Mul move: {} *= {}'.format(idx_generator, best_gen) self_gens[idx_generator] *= best_gen return self
def pad(self, extra_bits=0, lower_right=None): r""" Takes a PauliList, and returns a new PauliList, appending ``extra_bits`` qubits, with stabilizer operators specified by ``lower_right``. :arg pauli_list_in: list of Pauli operators to be padded. :param int extra_bits: Number of extra bits to be appended to the system. :param lower_right: list of `qecc.Pauli` operators, acting on `extra_bits` qubits. :rtype: list of :class:`qecc.Pauli` objects. Example: >>> import qecc as q >>> pauli_list = q.PauliList('XXX', 'YIY', 'ZZI') >>> pauli_list.pad(extra_bits=2, lower_right=q.PauliList('IX','ZI')) PauliList(i^0 XXXII, i^0 YIYII, i^0 ZZIII, i^0 IIIIX, i^0 IIIZI) """ len_P = len(self) nq_P = len(self[0]) if len_P > 0 else 0 if extra_bits == 0 and lower_right is None or len(lower_right) == 0: return PauliList(self) elif len(lower_right) != 0: extra_bits=len(lower_right[0]) setout = PauliList([pc.Pauli(pauli.op + 'I'*extra_bits) for pauli in self]) if lower_right is None: setout += [pc.eye_p(nq_P + extra_bits)] * extra_bits else: setout += [pc.eye_p(nq_P) & P for P in lower_right] return setout
def syndrome_to_recovery_operator(self,synd): r""" Returns a Pauli operator which corrects an error on the stabilizer code ``self``, given the syndrome ``synd``, a bitstring indicating which generators the implied error commutes with and anti-commutes with. :param synd: a string, list, tuple or other sequence type with entries consisting only of 0 or 1. This parameter will be certified before use. """ # If the syndrome is an integer, change it to a bitstring by # using string formatting. if isinstance(synd,int): fmt = "{{0:0>{n}b}}".format(n=self.n_constraints) synd = fmt.format(synd) # Ensures synd is a list of integers by mapping int onto the list. synd=map(int, synd) # Check that the syndrome is all zeros and ones. acceptable_syndrome = all([bit == 0 or bit == 1 for bit in synd]) if not acceptable_syndrome: raise ValueError("Please input a syndrome which is an iterable onto 0 and 1.") if len(synd) != self.nq - self.nq_logical: raise ValueError("Syndrome must account for n-k bits of syndrome data.") # We produce commutation and anti_commutation constraints from synd. anti_coms = list(it.compress(self.group_generators,synd)) coms = list(it.compress(self.group_generators,[1-bit for bit in synd])) for op_weight in range(self.nq+1): #We loop over all possible weights. As soon as we find an operator #that satisfies the commutation and anti-commutation constraints, #we return it: low_weight_ops=map(p.remove_phase, cs.solve_commutation_constraints(coms,anti_coms, search_in_set=p.paulis_by_weight(self.nq, op_weight))) if low_weight_ops: break return low_weight_ops[0]
def centralizer_gens(self, group_gens=None): r""" Returns the generators of the centralizer group :math:`\mathrm{C}(P_1, \dots, P_k)`, where :math:`P_i` is the :math:`i^{\text{th}}` element of this list. See :meth:`qecc.Pauli.centralizer_gens` for more information. """ if group_gens is None: # NOTE: Assumes all Paulis contained by self have the same nq. Xs, Zs = pc.elem_gens(len(self[0])) group_gens = Xs + Zs if len(self) == 0: # C({}) = G return PauliList(group_gens) centralizer_0 = self[0].centralizer_gens(group_gens=group_gens) if len(self) == 1: return centralizer_0 else: return self[1:].centralizer_gens(group_gens=centralizer_0)
def __and__(self, other): """Returns the Kronecker product of two stabilizer codes, given each of the constituent codes. """ if not isinstance(other, StabilizerCode): return NotImplemented return StabilizerCode( (self.group_generators & p.eye_p(other.nq)) + (p.eye_p(self.nq) & other.group_generators), (self.logical_xs & p.eye_p(other.nq)) + (p.eye_p(self.nq) & other.logical_xs), (self.logical_zs & p.eye_p(other.nq)) + (p.eye_p(self.nq) & other.logical_zs), )
def star_decoder(self, for_enc=None, as_dict=False): r""" Returns a tuple of a decoding Clifford and a :class:`qecc.PauliList` specifying the recovery operation to perform as a function of the result of a :math:`Z^{\otimes{n - k}}` measurement on the ancilla register. For syndromes corresponding to errors of weight greater than the distance, the relevant element of the recovery list will be set to :obj:`qecc.Unspecified`. :param for_enc: If not ``None``, specifies to use a given Clifford operator as the encoder, instead of the first element yielded by :meth:`encoding_cliffords`. :param bool as_dict: If ``True``, returns a dictionary from recovery operators to syndromes that indicate that recovery. """ def error_to_pauli(error): if error == p.I.as_clifford(): return "I" if error == p.X.as_clifford(): return "X" if error == p.Y.as_clifford(): return "Y" if error == p.Z.as_clifford(): return "Z" if for_enc is None: encoder = self.encoding_cliffords().next() else: encoder = for_enc decoder = encoder.inv() errors = pc.PauliList(p.eye_p(self.nq)) + pc.PauliList(p.paulis_by_weight(self.nq, self.n_correctable)) syndrome_dict = defaultdict(lambda: Unspecified) syndrome_meas = [p.elem_gen(self.nq, idx, 'Z') for idx in range(self.nq_logical, self.nq)] for error in errors: effective_gate = decoder * error.as_clifford() * encoder # FIXME: the following line emulates measurement until we have a real # measurement simulation method. syndrome = tuple([effective_gate(meas).ph / 2 for meas in syndrome_meas]) recovery = "".join([ # FIXME: the following is a broken hack to get the phases on the logical qubit register. error_to_pauli(c.Clifford([effective_gate.xout[idx][idx]], [effective_gate.zout[idx][idx]])) for idx in range(self.nq_logical) ]) # For degenerate codes, the syndromes can collide, so long as we # correct the same way for each. if syndrome in syndrome_dict and syndrome_dict[syndrome] != recovery: raise RuntimeError('Syndrome {} has collided.'.format(syndrome)) syndrome_dict[syndrome] = recovery if as_dict: outdict = dict() keyfn = lambda (syndrome, recovery): recovery data = sorted(syndrome_dict.items(), key=keyfn) for recovery, syndrome_group in it.groupby(data, keyfn): outdict[recovery] = [syn[0] for syn in syndrome_group] return decoder, outdict else: recovery_list = pc.PauliList(syndrome_dict[syndrome] for syndrome in it.product(range(2), repeat=self.n_constraints)) return decoder, recovery_list
def pred_fn(P): # Using imap here instead of map allows all() to short-circuit. return all(it.imap(lambda Q: pc.com(P, Q) == 0, paulis))
def solve_commutation_constraints(commutation_constraints=[], anticommutation_constraints=[], search_in_gens=None, search_in_set=None): r""" Given commutation constraints on a Pauli operator, yields an iterator onto all solutions of those constraints. :param commutation_constraints: A list of operators :math:`\{A_i\}` such that each solution :math:`P` yielded by this function must satisfy :math:`[A_i, P] = 0` for all :math:`i`. :param anticommutation_constraints: A list of operators :math:`\{B_i\}` such that each solution :math:`P` yielded by this function must satisfy :math:`\{B_i, P\} = 0` for all :math:`i`. :param search_in_gens: A list of operators :math:`\{N_i\}` that generate the group in which to search for solutions. If ``None``, defaults to the elementary generators of the pc.Pauli group on :math:`n` qubits, where :math:`n` is given by the length of the commutation and anticommutation constraints. :param search_in_set: An iterable of operators to which the search for satisfying assignments is restricted. This differs from ``search_in_gens`` in that it specifies the entire set, not a generating set. When this parameter is specified, a brute-force search is executed. Use only when the search set is small, and cannot be expressed using its generating set. :returns: An iterator ``it`` such that ``list(it)`` contains all operators within the group :math:`G = \langle N_1, \dots, N_k \rangle` given by ``search_in_gens``, consistent with the commutation and anticommutation constraints. This function is based on finding the generators of the centralizer groups of each commutation constraint, and is thus faster than a predicate-based search over the entire group of interest. The resulting iterator can be used in conjunction with other filters, however. >>> import qecc as q >>> list(q.solve_commutation_constraints(q.PauliList('XXI', 'IZZ', 'IYI'), q.PauliList('YIY'))) [i^0 XII, i^0 IIZ, i^0 YYX, i^0 ZYY] >>> from itertools import ifilter >>> list(ifilter(lambda P: P.wt <= 2, q.solve_commutation_constraints(q.PauliList('XXI', 'IZZ', 'IYI'), q.PauliList('YIY')))) [i^0 XII, i^0 IIZ] """ # Normalize our arguments to be PauliLists, so that we can obtain # centralizers easily. if not isinstance(commutation_constraints, PauliList): commutation_constraints = PauliList(commutation_constraints) if not isinstance(anticommutation_constraints, PauliList): # This is probably not necessary, strictly speaking, but it keeps me # slightly more sane to have both constraints represented by the same # sequence type. anticommutation_constraints = PauliList(anticommutation_constraints) # Then check that the arguments make sense. if len(commutation_constraints) == 0 and len( anticommutation_constraints) == 0: raise ValueError("At least one constraint must be specified.") #We default to executing a brute-force search if the search set is #explicitly specified: if search_in_set is not None: commutation_predicate = AllPredicate( *[(lambda P: pc.com(P, acc) == 0) for acc in commutation_constraints]) commuters = list(filter(commutation_predicate, search_in_set)) anticommutation_predicate = AllPredicate( *[(lambda P: pc.com(P, acc) == 1) for acc in anticommutation_constraints]) return list(filter(anticommutation_predicate, commuters)) # We finish putting arguments in the right form by defaulting to searching # over the pc.Pauli group on $n$ qubits. if search_in_gens is None: nq = len(commutation_constraints[0] if len(commutation_constraints) > 0 else anticommutation_constraints[0]) Xs, Zs = pc.elem_gens(nq) search_in_gens = Xs + Zs # Now we update our search by restricting to the centralizer of the # commutation constraints. search_in_gens = commutation_constraints.centralizer_gens( group_gens=search_in_gens) # Finally, we return a filter iterator on the elements of the given # centralizer that selects elements which anticommute appropriately. anticommutation_predicate = AllPredicate( *[(lambda P: pc.com(P, acc) == 1) for acc in anticommutation_constraints]) assert len(search_in_gens) > 0 return filter(anticommutation_predicate, pc.from_generators(search_in_gens))
def generated_group(self, coset_rep=None): """ Yields an iterator onto the group generated by this list of Pauli operators. See also :obj:`qecc.from_generators`. """ return pc.from_generators(self, coset_rep)
def __init__(self, S, ignore_phase=True): super(PauliMembershipPredicate, self).__init__( map(lambda P: pc.Pauli(P.op), S) if ignore_phase else S) self.ignore_phase = ignore_phase
def clifford_as_unitary(C): nq = len(C) dim = 2**nq U = np.zeros((dim,dim), dtype='complex') psi_0 = mutual_eigenspace(map(pauli_as_unitary, C.zout)).T for b in xrange(dim): bits = '{{0:0>{nq}b}}'.format(nq=nq).format(b) Xb = reduce(op.mul, (C.xout[idx] for idx in xrange(nq) if bits[idx] == '1'), pc.eye_p(nq)).as_unitary() for a in xrange(dim): bra_a = np.zeros((1, dim)) bra_a[0, a] = 1 U[a, b] = reduce(np.dot, [bra_a, Xb, psi_0])[0,0] return U
Returns a predicate that selects Pauli operators in the group generated by a given list of generators. """ # Warning: This is inefficient for large groups! paulis = map(pc.ensure_pauli, paulis) return PauliMembershipPredicate(pc.from_generators(paulis), ignore_phase=True) ## TEST ## if __name__ == "__main__": p = Predicate(lambda x: x > 0) q = Predicate(lambda x: x < 3) p_and_q = p & q p_or_q = p | q not_p = ~p for test in [2, 4, -1]: print test, p(test), q(test), p_and_q(test), p_or_q(test), not_p(test) print filter(p_and_q, range(-4, 5)) S = set([1, 2, 3]) in_S = SetMembershipPredicate(S) print map(in_S, range(-1, 5)) print filter(commutes_with("XX", "ZZ") & ~in_group_generated_by("XX"), pc.pauli_group(2))
def __call__(self, P): if self.ignore_phase: P = pc.Pauli(P.op) return P in self.S
class Location(object): """ Represents a gate, wait, measurement or preparation location in a circuit. Note that currently, only gate locations are implemented. :param kind: The kind of location to be created. Each kind is an abbreviation drawn from ``Location.KIND_NAMES``, or is the index in ``Location.KIND_NAMES`` corresponding to the desired location kind. :type kind: int or str :param qubits: Indicies of the qubits on which this location acts. :type qubits: tuple of ints. """ ## PRIVATE CLASS CONSTANTS ## _CLIFFORD_GATE_KINDS = [ 'I', 'X', 'Y', 'Z', 'H', 'R_pi4', 'CNOT', 'CZ', 'SWAP' ] _CLIFFORD_GATE_FUNCS = { 'I': lambda nq, idx: cc.eye_c(nq), 'X': lambda nq, idx: pc.elem_gen(nq, idx, 'X').as_clifford(), 'Y': lambda nq, idx: pc.elem_gen(nq, idx, 'Y').as_clifford(), 'Z': lambda nq, idx: pc.elem_gen(nq, idx, 'Z').as_clifford(), 'H': cc.hadamard, 'R_pi4': cc.phase, 'CNOT': cc.cnot, 'CZ': cc.cz, 'SWAP': cc.swap } _QCVIEWER_NAMES = { 'I': 'I', # This one is implemented by a gate definition # included by Circuit.as_qcviewer(). 'X': 'X', 'Y': 'Y', 'Z': 'Z', 'H': 'H', 'R_pi4': 'P', 'CNOT': 'tof', 'CZ': 'Z', 'SWAP': 'swap' } ## PUBLIC CLASS CONSTANTS ## #: Names of the kinds of locations used by QuaEC. KIND_NAMES = sum([_CLIFFORD_GATE_KINDS], []) ## INITIALIZER ## def __init__(self, kind, *qubits): if isinstance(kind, int): self._kind = kind elif isinstance(kind, str): self._kind = self.KIND_NAMES.index(kind) else: raise TypeError("Location kind must be an int or str.") #if not all(isinstance(q, int) for q in qubits): # raise TypeError('Qubit indices must be integers. Got {} instead, which is of type {}.'.format( # *(iter((q, type(q)) for q in qubits if not isinstance(q, int)).next()) # )) try: self._qubits = tuple(map(int, qubits)) except TypeError as e: raise TypeError('Qubit integers must be int-like.') self._is_clifford = bool(self.kind in self._CLIFFORD_GATE_KINDS) ## REPRESENTATION METHODS ## def __str__(self): return " {:<4} {}".format(self.kind, ' '.join(map(str, self.qubits))) def __repr__(self): return "<{} Location on qubits {}>".format(self.kind, self.qubits) def __hash__(self): return hash((self._kind, ) + self.qubits) ## IMPORT METHODS ## @staticmethod def from_quasm(source): """ Returns a :class:`qecc.Location` initialized from a QuASM-formatted line. :type str source: A line of QuASM code specifying a location. :rtype: :class:`qecc.Location` :returns: The location represented by the given QuASM source. """ parts = source.split() return Location(parts[0], *map(int, parts[1:])) ## PROPERTIES ## @property def kind(self): """ Returns a string defining which kind of location this instance represents. Guaranteed to be a string that is an element of ``Location.KIND_NAMES``. """ return self.KIND_NAMES[self._kind] @property def qubits(self): """ Returns a tuple of ints describing which qubits this location acts upon. """ return self._qubits @property def nq(self): """ Returns the number of qubits in the smallest circuit that can contain this location without relabeling qubits. For a :class:`qecc.Location` ``loc``, this property is defined as ``1 + max(loc.nq)``. """ return 1 + max(self.qubits) @property def is_clifford(self): """ Returns ``True`` if and only if this location represents a gate drawn from the Clifford group. """ return self._is_clifford @property def wt(self): """ Returns the number of qubits on which this location acts. """ return len(self.qubits) ## SIMULATION METHODS ## def as_clifford(self, nq=None): """ If this location represents a Clifford gate, returns the action of that gate. Otherwise, a :obj:`RuntimeError` is raised. :param int nq: Specifies how many qubits to represent this location as acting upon. If not specified, defaults to the value of the ``nq`` property. :rtype: :class:`qecc.Clifford` """ if not self.is_clifford: raise RuntimeError("Location must be a Clifford gate.") else: if nq is None: nq = self.nq elif nq < self.nq: raise ValueError( 'nq must be greater than or equal to the nq property.') return self._CLIFFORD_GATE_FUNCS[self.kind](nq, *self.qubits) ## EXPORT METHODS ## def as_qcviewer(self, qubit_names=None): """ Returns a representation of this location in a format suitable for inclusion in a QCViewer file. :param qubit_names: If specified, the given aliases will be used for the qubits involved in this location when exporting to QCViewer. Defaults to "q1", "q2", etc. :rtype: str Note that the identity (or "wait") location requires the following to be added to QCViewer's ``gateLib``:: NAME wait DRAWNAME "1" SYMBOL I 1 , 0 0 , 1 """ # FIXME: link to QCViewer in the docstring here. return ' {gatename} {gatespec}\n'.format( gatename=self._QCVIEWER_NAMES[self.kind], gatespec=qubits_str(self.qubits, qubit_names), ) ## OTHER METHODS ## def relabel_qubits(self, relabel_dict): """ Returns a new location related to this one by a relabeling of the qubits. The relabelings are to be indicated by a dictionary that specifies what each qubit index is to be mapped to. >>> import qecc as q >>> loc = q.Location('CNOT', 0, 1) >>> print loc CNOT 0 1 >>> print loc.relabel_qubits({1: 2}) CNOT 0 2 :param dict relabel_dict: If `i` is a key of `relabel_dict`, then qubit `i` will be replaced by `relabel_dict[i]` in the returned location. :rtype: :class:`qecc.Location` :returns: A new location with the qubits relabeled as specified by `relabel_dict`. """ return Location( self.kind, *tuple(relabel_dict[i] if i in relabel_dict else i for i in self.qubits))
def solve_commutation_constraints( commutation_constraints=[], anticommutation_constraints=[], search_in_gens=None, search_in_set=None ): r""" Given commutation constraints on a Pauli operator, yields an iterator onto all solutions of those constraints. :param commutation_constraints: A list of operators :math:`\{A_i\}` such that each solution :math:`P` yielded by this function must satisfy :math:`[A_i, P] = 0` for all :math:`i`. :param anticommutation_constraints: A list of operators :math:`\{B_i\}` such that each solution :math:`P` yielded by this function must satisfy :math:`\{B_i, P\} = 0` for all :math:`i`. :param search_in_gens: A list of operators :math:`\{N_i\}` that generate the group in which to search for solutions. If ``None``, defaults to the elementary generators of the pc.Pauli group on :math:`n` qubits, where :math:`n` is given by the length of the commutation and anticommutation constraints. :param search_in_set: An iterable of operators to which the search for satisfying assignments is restricted. This differs from ``search_in_gens`` in that it specifies the entire set, not a generating set. When this parameter is specified, a brute-force search is executed. Use only when the search set is small, and cannot be expressed using its generating set. :returns: An iterator ``it`` such that ``list(it)`` contains all operators within the group :math:`G = \langle N_1, \dots, N_k \rangle` given by ``search_in_gens``, consistent with the commutation and anticommutation constraints. This function is based on finding the generators of the centralizer groups of each commutation constraint, and is thus faster than a predicate-based search over the entire group of interest. The resulting iterator can be used in conjunction with other filters, however. >>> import qecc as q >>> list(q.solve_commutation_constraints(q.PauliList('XXI', 'IZZ', 'IYI'), q.PauliList('YIY'))) [i^0 XII, i^0 IIZ, i^0 YYX, i^0 ZYY] >>> from itertools import ifilter >>> list(ifilter(lambda P: P.wt <= 2, q.solve_commutation_constraints(q.PauliList('XXI', 'IZZ', 'IYI'), q.PauliList('YIY')))) [i^0 XII, i^0 IIZ] """ # Normalize our arguments to be PauliLists, so that we can obtain # centralizers easily. if not isinstance(commutation_constraints, PauliList): commutation_constraints = PauliList(commutation_constraints) if not isinstance(anticommutation_constraints, PauliList): # This is probably not necessary, strictly speaking, but it keeps me # slightly more sane to have both constraints represented by the same # sequence type. anticommutation_constraints = PauliList(anticommutation_constraints) # Then check that the arguments make sense. if len(commutation_constraints) == 0 and len(anticommutation_constraints) == 0: raise ValueError("At least one constraint must be specified.") #We default to executing a brute-force search if the search set is #explicitly specified: if search_in_set is not None: commutation_predicate = AllPredicate(*map( lambda acc: (lambda P: pc.com(P, acc) == 0), commutation_constraints )) commuters = filter(commutation_predicate, search_in_set) anticommutation_predicate = AllPredicate(*map( lambda acc: (lambda P: pc.com(P, acc) == 1), anticommutation_constraints )) return filter(anticommutation_predicate, commuters) # We finish putting arguments in the right form by defaulting to searching # over the pc.Pauli group on $n$ qubits. if search_in_gens is None: nq = len(commutation_constraints[0] if len(commutation_constraints) > 0 else anticommutation_constraints[0]) Xs, Zs = pc.elem_gens(nq) search_in_gens = Xs + Zs # Now we update our search by restricting to the centralizer of the # commutation constraints. search_in_gens = commutation_constraints.centralizer_gens(group_gens=search_in_gens) # Finally, we return a filter iterator on the elements of the given # centralizer that selects elements which anticommute appropriately. anticommutation_predicate = AllPredicate(*map( lambda acc: (lambda P: pc.com(P, acc) == 1), anticommutation_constraints )) assert len(search_in_gens) > 0 return ifilter(anticommutation_predicate, pc.from_generators(search_in_gens))
return PauliMembershipPredicate(pc.from_generators(paulis), ignore_phase=True) ## TEST ## if __name__ == "__main__": p = Predicate(lambda x: x > 0) q = Predicate(lambda x: x < 3) p_and_q = p & q p_or_q = p | q not_p = ~p for test in [2, 4, -1]: print(test, p(test), q(test), p_and_q(test), p_or_q(test), not_p(test)) print(list(filter(p_and_q, list(range(-4, 5))))) S = set([1, 2, 3]) in_S = SetMembershipPredicate(S) print(list(map(in_S, list(range(-1, 5))))) print( list( filter( commutes_with('XX', 'ZZ') & ~in_group_generated_by('XX'), pc.pauli_group(2))))
# Warning: This is inefficient for large groups! paulis = list(map(pc.ensure_pauli, paulis)) return PauliMembershipPredicate(pc.from_generators(paulis), ignore_phase=True) ## TEST ## if __name__ == "__main__": p = Predicate(lambda x: x > 0) q = Predicate(lambda x: x < 3) p_and_q = p & q p_or_q = p | q not_p = ~p for test in [2, 4, -1]: print(test, p(test), q(test), p_and_q(test), p_or_q(test), not_p(test)) print(list(filter(p_and_q, list(range(-4,5))))) S = set([1, 2, 3]) in_S = SetMembershipPredicate(S) print(list(map(in_S, list(range(-1, 5))))) print(list(filter( commutes_with('XX', 'ZZ') & ~in_group_generated_by('XX'), pc.pauli_group(2) )))
def __init__(self, S, ignore_phase=True): super(PauliMembershipPredicate, self).__init__([pc.Pauli(P.op) for P in S] if ignore_phase else S) self.ignore_phase = ignore_phase
def star_decoder(self, for_enc=None, as_dict=False): r""" Returns a tuple of a decoding Clifford and a :class:`qecc.PauliList` specifying the recovery operation to perform as a function of the result of a :math:`Z^{\otimes{n - k}}` measurement on the ancilla register. For syndromes corresponding to errors of weight greater than the distance, the relevant element of the recovery list will be set to :obj:`qecc.Unspecified`. :param for_enc: If not ``None``, specifies to use a given Clifford operator as the encoder, instead of the first element yielded by :meth:`encoding_cliffords`. :param bool as_dict: If ``True``, returns a dictionary from recovery operators to syndromes that indicate that recovery. """ def error_to_pauli(error): if error == p.I.as_clifford(): return "I" if error == p.X.as_clifford(): return "X" if error == p.Y.as_clifford(): return "Y" if error == p.Z.as_clifford(): return "Z" if for_enc is None: encoder = self.encoding_cliffords().next() else: encoder = for_enc decoder = encoder.inv() errors = pc.PauliList(p.eye_p(self.nq)) + pc.PauliList( p.paulis_by_weight(self.nq, self.n_correctable)) syndrome_dict = defaultdict(lambda: Unspecified) syndrome_meas = [ p.elem_gen(self.nq, idx, 'Z') for idx in range(self.nq_logical, self.nq) ] for error in errors: effective_gate = decoder * error.as_clifford() * encoder # FIXME: the following line emulates measurement until we have a real # measurement simulation method. syndrome = tuple( [effective_gate(meas).ph / 2 for meas in syndrome_meas]) recovery = "".join([ # FIXME: the following is a broken hack to get the phases on the logical qubit register. error_to_pauli( c.Clifford([effective_gate.xout[idx][idx]], [effective_gate.zout[idx][idx]])) for idx in range(self.nq_logical) ]) # For degenerate codes, the syndromes can collide, so long as we # correct the same way for each. if syndrome in syndrome_dict and syndrome_dict[ syndrome] != recovery: raise RuntimeError( 'Syndrome {} has collided.'.format(syndrome)) syndrome_dict[syndrome] = recovery if as_dict: outdict = dict() keyfn = lambda (syndrome, recovery): recovery data = sorted(syndrome_dict.items(), key=keyfn) for recovery, syndrome_group in it.groupby(data, keyfn): outdict[recovery] = [syn[0] for syn in syndrome_group] return decoder, outdict else: recovery_list = pc.PauliList( syndrome_dict[syndrome] for syndrome in it.product( range(2), repeat=self.n_constraints)) return decoder, recovery_list