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 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 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 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 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 = next( c.Clifford(cliff_xouts_left, cliff_zouts_left).constraint_completions()) 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) + [ elem & p.eye_p(nq_anc) for elem in xs_right ] temp_zouts_right = [Unspecified] * len(stab_left) + [ elem & p.eye_p(nq_anc) for elem in 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 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=next(c.Clifford(cliff_xouts_left,cliff_zouts_left).constraint_completions()) 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) + [elem & p.eye_p(nq_anc) for elem in xs_right] temp_zouts_right = [Unspecified]*len(stab_left) + [elem & p.eye_p(nq_anc) for elem in 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 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 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 += list( 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=list(map(self.block_logical_pauli, other.logical_xs)), logical_zs=list(map(self.block_logical_pauli, other.logical_zs)))
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 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 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
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 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 = next(self.encoding_cliffords()) 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: syndrome_recovery[1] data = sorted(list(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(list(range(2)), repeat=self.n_constraints)) return decoder, recovery_list
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