Ejemplo n.º 1
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))
Ejemplo n.º 2
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)
     )
Ejemplo n.º 3
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=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)
Ejemplo n.º 4
0
 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)
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
 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)
     )
Ejemplo n.º 7
0
 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],
         [], []
     )
Ejemplo n.º 8
0
 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)
     )
Ejemplo n.º 9
0
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
    )
Ejemplo n.º 10
0
 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)
Ejemplo n.º 11
0
 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
Ejemplo n.º 12
0
    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    
Ejemplo n.º 13
0
    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]
Ejemplo n.º 14
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)
Ejemplo n.º 15
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),
     )
Ejemplo n.º 16
0
 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
Ejemplo n.º 17
0
 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))
Ejemplo n.º 18
0
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))
Ejemplo n.º 19
0
 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)
Ejemplo n.º 20
0
 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
Ejemplo n.º 21
0
 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)
Ejemplo n.º 22
0
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
Ejemplo n.º 23
0
    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))
Ejemplo n.º 24
0
 def __call__(self, P):
     if self.ignore_phase:
         P = pc.Pauli(P.op)
     return P in self.S
Ejemplo n.º 25
0
 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))
Ejemplo n.º 26
0
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))
Ejemplo n.º 27
0
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))
Ejemplo n.º 28
0
    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))))
Ejemplo n.º 29
0
    # 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)
        )))
    
Ejemplo n.º 30
0
 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
Ejemplo n.º 31
0
    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