Source code for qecc.circuit

#!/usr/bin/python
# -*- coding: utf-8 -*-
##
# circuit.py: Modeling for stabilizer circuits.
##
# © 2012 Christopher E. Granade (cgranade@gmail.com) and
#     Ben Criger (bcriger@gmail.com).
# This file is a part of the QuaEC project.
# Licensed under the AGPL version 3.
##
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
##

## IMPORTS ##
from sys import version_info
if version_info[0] == 3:
    PY3 = True
    from importlib import reload
elif version_info[0] == 2:
    PY3 = False
else:
    raise EnvironmentError("sys.version_info refers to a version of "
        "Python neither 2 nor 3. This is not permitted. "
        "sys.version_info = {}".format(version_info))

from copy import copy
from operator import add, mul
import itertools as it
from functools import reduce

if PY3:
    from . import PauliClass as pc
    from . import CliffordClass as cc
    from . import utils as u
else:
    import PauliClass as pc
    import CliffordClass as cc
    import utils as u

## ALL ##

__all__ = [
    'Location', 'Circuit',
    'ensure_loc', 'propagate_fault', 'possible_output_faults', 'possible_faults'
]

## INTERNAL FUNCTIONS ##

def qubits_str(qubits, qubit_names=None):
    if qubit_names is None:
        return ' '.join('q{}'.format(idx + 1) for idx in qubits)
    else:
        return ' '.join(qubit_names[idx] for idx in qubits)

## CLASSES ##

[docs]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 ##
[docs] @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], *list(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 ##
[docs] 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 ##
[docs] 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 ##
[docs] 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 ensure_loc(loc): if isinstance(loc, tuple): loc = Location(*loc) elif not isinstance(loc, Location): raise TypeError('Locations must be specified either as Location instances or as tuples.') return loc
[docs]class Circuit(list): def __init__(self, *locs): # Circuit(('CNOT', 0, 2), ('H', 1)) works, but # Circuit('CNOT', 0, 2) doesn't work. list.__init__(self, list(map(ensure_loc, locs))) ## SEQUENCE PROTOCOL ##
[docs] def append(self, newval): super(Circuit, self).append(ensure_loc(newval))
append.__doc__ = list.append.__doc__
[docs] def insert(self, at, newval): super(Circuit, self).insert(at, ensure_loc(newval))
insert.__doc__ = list.insert.__doc__ def __getitem__(self, *args): item = super(Circuit, self).__getitem__(*args) if not isinstance(item, list): return item else: return Circuit(*item) def __getslice__(self, *args): return Circuit(*super(Circuit, self).__getslice__(*args)) def __add__(self, other): if not isinstance(other, Circuit): other = Circuit(*other) return Circuit(*super(Circuit, self).__add__(other)) def __iadd__(self, other): if not isinstance(other, Circuit): other = Circuit(*other) return Circuit(*super(Circuit, self).__iadd__(other)) ## PROPERTIES ## @property def nq(self): """ Returns the number of qubits on which this circuit acts. """ return max(loc.nq for loc in self) if self else 0 @property def size(self): """ Returns the number of locations in this circuit. Note that this property is synonymous with :obj:`len`, in that ``len(circ) == circ.size`` for all :class:`qecc.Circuit` instances. """ return len(self) @property def depth(self): """ Returns the minimum number of timesteps required to implement exactly this circuit in parallel. """ return len(list(self.group_by_time())) ## IMPORT CLASS METHODS ##
[docs] @staticmethod def from_quasm(source): """Returns a :class:`qecc.Circuit` object from a QuASM-formatted file, producing one location per line.""" if not isinstance(source, str): # Assume source is a file-like, so that iter(source) returns lines # in the file. it = iter(source) else: it = iter(source.split('\n')) return Circuit(*list(map(Location.from_quasm, it)))
## PRETTY PRINTING ## def __repr__(self): return "Circuit({})".format(", ".join(map(repr, self))) def __str__(self): return "\n".join(map(str, self))
[docs] def as_quasm(self): """ Returns a representation of the circuit in an assmembler-like format. In this format, each location is represented by a single line where the first field indicates the kind of location and the remaining fields indicate the qubits upon which the location acts. >>> import qecc as q >>> circ = q.Circuit(('CNOT', 0, 2), ('H', 2), ('SWAP', 1, 2), ('I', 0)) >>> print circ.as_quasm() CNOT 0 2 H 2 SWAP 1 2 I 0 """ return str(self)
[docs] def as_qcviewer(self, inputs=(0,), outputs=(0,), qubit_names=None): """ Returns a string representing this circuit in the format recognized by `QCViewer`_. :param tuple inputs: Specifies which qubits should be marked as inputs in the exported QCViewer circuit. :param tuple outputs: Specifies which qubits should be marked as outputs in the exported QCViewer circuit. :param qubit_names: Names to be used for each qubit when exporting to QCViewer. .. _QCViewer: http://qcirc.iqc.uwaterloo.ca/index.php?n=Projects.QCViewer """ header = '.v ' + qubits_str(list(range(self.nq)), qubit_names) + '\n' header += '.i ' + qubits_str(inputs, qubit_names) + '\n' header += '.o ' + qubits_str(outputs, qubit_names) + '\n' circ_text = 'BEGIN\n' for loc in self: circ_text += loc.as_qcviewer(qubit_names) circ_text += 'END\n' return header + circ_text
[docs] def as_qcircuit(self, C=None, R=None): r""" Typesets this circuit using the `Qcircuit`_ package for :math:`\text{\LaTeX}`. :param float C: Width (in ems) of each column. :param float R: Height (in ems) of each column. :rtype: :obj:`str` :returns: A string containing :math:`\text{\LaTeX}` source code for use with `Qcircuit`_. .. _Qcircuit: http://www.cquic.org/Qcircuit/ """ trans_cells = [] for timestep in self.group_by_time(): col = [r'\qw'] * self.nq # If nothing else, place a \qw. hidden_qubits = set() for loc in timestep: if any(qubit in hidden_qubits for qubit in range(min(loc.qubits), max(loc.qubits)+1)): # A qubit is hidden, so append and reset. trans_cells.append(col) col = [r'\qw'] * self.nq # If nothing else, place a \qw. hidden_qubits = set() if loc.wt == 1: col[loc.qubits[0]] = r"\gate{{{0}}}".format(loc.kind if loc.kind != "I" else r"\id") elif loc.kind == 'CNOT': col[loc.qubits[0]] = r'\ctrl{{{0}}}'.format(loc.qubits[1] - loc.qubits[0]) col[loc.qubits[1]] = r'\targ' else: raise NotImplementedError("Location kind {0.kind} not supported by this method.".format(loc)) hidden_qubits.update(list(range(min(loc.qubits), max(loc.qubits)+1))) trans_cells.append(col) cells = u.transpose([[''] * self.nq] + trans_cells + [[r'\qw'] * self.nq]) return r""" \Qcircuit {C} {R} {{ {0} }} """.format(u.latex_array_contents(cells), C="@C{}em".format(C) if C is not None else "", R="@R{}em".format(R) if R is not None else "" )
## CIRCUIT SIMULATION METHODS ##
[docs] def as_clifford(self): """ If this circuit is composed entirely of Clifford operators, converts it to a :class:`qecc.Clifford` instance representing the action of the entire circuit. If the circuit is not entirely Clifford gates, this method raises a :obj:`RuntimeError`. """ if not all(loc.is_clifford for loc in self): raise RuntimeError('All locations must be Clifford gates in order to represent a circuit as a Clifford operator.') nq = self.nq return reduce(mul, (loc.as_clifford(nq) for loc in reversed(self)), cc.eye_c(nq))
## CIRCUIT SIMPLIFICATION METHODS ##
[docs] def cancel_selfinv_gates(self, start_at=0): """ Transforms the circuit, removing any self-inverse gates from the circuit if possible. Note that not all self-inverse gates are currently supported by this method. :param int start_at: Specifies which location to consider first. Any locations before ``start_at`` are not considered for cancelation by this method. """ SELFINV_GATES = ['H', 'X', 'Y', 'Z', 'CNOT'] if start_at == len(self): return self loc = self[start_at] if loc.kind in SELFINV_GATES: if len(loc.qubits) == 1: # TODO: add two-qubit gates. q = loc.qubits[0] for idx_future in range(start_at + 1, len(self)): if q in self[idx_future].qubits: # Check that the kind matches. if self[idx_future].kind == loc.kind: self.pop(idx_future) self.pop(start_at) return self.cancel_selfinv_gates(start_at=start_at) else: # Go on to the next gate, since there's another gate # between here. return self.cancel_selfinv_gates(start_at=start_at+1) return self.cancel_selfinv_gates(start_at=start_at+1)
[docs] def replace_cz_by_cnot(self): """ Changes all controlled-:math:`Z` gates in this circuit to controlled-NOT gates, adding Hadamard locations as required. """ # FIXME: this is inefficient as hell right now. try: idx = next((idx for idx in range(len(self)) if self[idx].kind == 'CZ')) q = self[idx].qubits self[idx] = Location('CNOT', *q) self.insert(idx + 1, ('H', q[1])) self.insert(idx, ('H', q[1])) return self.replace_cz_by_cnot() except StopIteration: return self
[docs] def group_by_time(self, pad_with_waits=False): """ Returns an iterator onto subcircuits of this circuit, each of depth 1. :param bool pad_with_waits: If ``True``, each subcircuit will have wait locations added such that every qubit is acted upon in every subcircuit. :yields: each depth-1 subcircuit, corresponding to time steps of the circuit """ nq = self.nq found = [False] * nq group_acc = Circuit() for loc in self: if any(found[qubit] for qubit in loc.qubits): if pad_with_waits: group_acc += [('I', qubit) for qubit in range(nq) if not found[qubit]] yield group_acc found = [False] * nq group_acc = Circuit() for qubit in loc.qubits: found[qubit] = True group_acc.append(loc) if pad_with_waits: group_acc += [('I', qubit) for qubit in range(nq) if not found[qubit]] yield group_acc
[docs] def pad_with_waits(self): """ Returns a copy of the :class:`qecc.Circuit` ``self``, which contains explicit wait locations. """ return sum(self.group_by_time(pad_with_waits=True), Circuit())
## OTHER METHODS ##
[docs] def relabel_qubits(self, relabel_dict): """ Returns a new circuit 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 circuit. :rtype: :class:`qecc.Circuit` :returns: A new circuit with the qubits relabeled as specified by `relabel_dict`. """ return Circuit(*[ loc.relabel_qubits(relabel_dict) for loc in self ])
## FUNCTIONS ##
[docs]def propagate_fault(circuitlist, fault): """ Given a list of circuits representing a list of timesteps (see :meth:`qecc.Circuit.group_by_time`) and a Pauli fault, propagates that fault through the remainder of the time-sliced circuit. :param list circuitlist: A list of :class:`qecc.Circuit` instances representing the timesteps of a larger circuit. :param qecc.Pauli fault: A Pauli fault to occur immediately before timestep ``timestep``. :param int timestep: The timestep immediately following when the fault to be propagated occured. :rtype: :class:`qecc.Pauli` :returns: The effective fault after propagating ``fault`` through the remainder of ``circuitlist``. """ fault_out = fault for step in circuitlist: fault_out = step.as_clifford().conjugate_pauli(fault_out) return fault_out
[docs]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 )
[docs]def possible_output_faults(circuitlist): """ Gives an iterator onto all possible effective faults due to 1-fault paths occuring within ``circuitlist``, assuming it has been padded with waits. :param list circuitlist: A list of :class:`qecc.Circuit` instances representing timesteps in a larger circuit. See :meth:`qecc.Circuit.group_by_time`. :yields: :class:`qecc.Pauli` instances representing possible effective faults due to 1-fault paths within the circuit represented by ``circuitlist``. """ outputs = iter([]) for timestep_idx in range(len(circuitlist)): outputs = it.imap( lambda fault: propagate_fault( circuitlist[timestep_idx+1:],fault), possible_faults( circuitlist[timestep_idx] )) for output in outputs: yield output