Protocols

View on QuantumAI Run in Google Colab View source on GitHub Download notebook
try:
    import cirq
except ImportError:
    print("installing cirq...")
    !pip install --quiet cirq
    print("installed cirq.")

Introduction

Cirq's protocols are very similar concept to Python's built-in protocols that were introduced in PEP 544. Python's built-in protocols are extremely convenient, for example behind all the for loops and list comprehensions you can find the Iterator protocol. As long as an object has the __iter__() magic method that returns an iterator object, it has iterator support. An iterator object has to define __iter__() and __next__() magic methods, that defines the iterator protocol. The iter(val) builtin function returns an iterator for val if it defines the above methods, otherwise throws a TypeError. Cirq protocols work similarly.

A canonical Cirq protocol example is the unitary protocol that allows to check the unitary matrix of values that support the protocol by calling cirq.unitary(val).

import cirq 

print(cirq.X)
print("cirq.X unitary:\n", cirq.unitary(cirq.X))

a, b = cirq.LineQubit.range(2)
circuit = cirq.Circuit(cirq.X(a), cirq.Y(b))
print(circuit)
print("circuit unitary:\n", cirq.unitary(circuit))

When an object does not support a given protocol, an error is thrown.

try: 
    print(cirq.unitary(a)) ## error!
except Exception as e: 
    print("As expected, a qubit does not have a unitary. The error: ")
    print(e)

What is a protocol?

A protocol is a combination of the following two items:

  • a SupportsXYZ class, which defines and documents all the magic functions that need to be implemented in order to support that given protocol
  • the entrypoint function(s), which are exposed to the main cirq namespace as cirq.xyz()

Cirq's protocols

For a complete list of Cirq protocols, refer to the cirq.protocols package. Here we provide a list of frequently used protocols for debugging, simulation and testing.

Protocol Description
cirq.apply_channel High performance evolution under a channel evolution.
cirq.apply_mixture High performance evolution under a mixture of unitaries evolution.
cirq.apply_unitaries Apply a series of unitaries onto a state tensor.
cirq.apply_unitary High performance left-multiplication of a unitary effect onto a tensor.
cirq.approx_eq Approximately compares two objects.
cirq.commutes Determines whether two values commute.
cirq.definitely_commutes Determines whether two values definitely commute.
cirq.decompose Recursively decomposes a value into cirq.Operations meeting a criteria.
cirq.decompose_once Decomposes a value into operations, if possible.
cirq.decompose_once_with_qubits Decomposes a value into operations on the given qubits.
cirq.equal_up_to_global_phase Determine whether two objects are equal up to global phase.
cirq.has_kraus Returns whether the value has a Kraus representation.
cirq.has_mixture Returns whether the value has a mixture representation.
cirq.has_unitary Determines whether the value has a unitary effect.
cirq.inverse Returns the inverse val**-1 of the given value, if defined.
cirq.is_measurement Determines whether or not the given value is a measurement.
cirq.is_parameterized Returns whether the object is parameterized with any Symbols.
cirq.kraus Returns a Kraus representation of the given channel.
cirq.measurement_key Get the single measurement key for the given value.
cirq.measurement_keys Gets the measurement keys of measurements within the given value.
cirq.mixture Return a sequence of tuples representing a probabilistic unitary.
cirq.num_qubits Returns the number of qubits, qudits, or qids val operates on.
cirq.parameter_names Returns parameter names for this object.
cirq.parameter_symbols Returns parameter symbols for this object.
cirq.pauli_expansion Returns coefficients of the expansion of val in the Pauli basis.
cirq.phase_by Returns a phased version of the effect.
cirq.pow Returns val**factor of the given value, if defined.
cirq.qasm Returns QASM code for the given value, if possible.
cirq.qid_shape Returns a tuple describing the number of quantum levels of each
cirq.quil Returns the QUIL code for the given value.
cirq.read_json Read a JSON file that optionally contains cirq objects.
cirq.resolve_parameters Resolves symbol parameters in the effect using the param resolver.
cirq.to_json Write a JSON file containing a representation of obj.
cirq.trace_distance_bound Returns a maximum on the trace distance between this effect's input
cirq.trace_distance_from_angle_list Given a list of arguments of the eigenvalues of a unitary matrix,
cirq.unitary Returns a unitary matrix describing the given value.
cirq.validate_mixture Validates that the mixture's tuple are valid probabilities.

Quantum operator representation protocols

The following family of protocols is an important and frequently used set of features of Cirq and it is worthwhile mentioning them and and how they interact with each other. They are, in the order of increasing generality:

  • *unitary
  • *kraus
  • *mixture

All these protocols make it easier to work with different representations of quantum operators, namely:

  • finding that representation (unitary, kraus, mixture),
  • determining whether the operator has that representation (has_*)
  • and applying them (apply_*) on a state vector.

Unitary

The *unitary protocol is the least generic, as only unitary operators should implement it. The cirq.unitary function returns the matrix representation of the operator in the computational basis. We saw an example of the unitary protocol above, but let's see the unitary matrix of the Pauli-Y operator as well:

print(cirq.unitary(cirq.Y))
[[0.+0.j 0.-1.j]
 [0.+1.j 0.+0.j]]

Mixture

The *mixture protocol should be implemented by operators that are unitary-mixtures. These probabilistic operators are represented by a list of tuples ($p_i$, $U_i$), where each unitary effect $U_i$ occurs with a certain probability $p_i$, and $\sum p_i = 1$. Probabilities are a Python float between 0.0 and 1.0, and the unitary matrices are numpy arrays.

Constructing simple probabilistic gates in Cirq is easiest with the with_probability method.

probabilistic_x = cirq.X.with_probability(.3)

for p, op in cirq.mixture(probabilistic_x):
    print(f"probability: {p}")
    print("operator:")
    print(op)
probability: 0.3
operator:
[[0.+0.j 1.+0.j]
 [1.+0.j 0.+0.j]]
probability: 0.7
operator:
[[1. 0.]
 [0. 1.]]

In case an operator does not implement SupportsMixture, but does implement SupportsUnitary, *mixture functions fall back to the *unitary methods. It is easy to see that a unitary operator $U$ is just a "mixture" of a single unitary with probability $p=1$.

# cirq.Y has a unitary effect but does not implement SupportsMixture
# thus mixture protocols will return ((1, cirq.unitary(Y)))
print(cirq.mixture(cirq.Y))
print(cirq.has_mixture(cirq.Y))
((1.0, array([[0.+0.j, 0.-1.j],
       [0.+1.j, 0.+0.j]])),)
True

Channel

The kraus representation is the operator sum representation of a quantum operator (a channel):

$$ \rho \rightarrow \sum_{k=0}^{r-1} A_k \rho A_k^\dagger $$

These matrices are required to satisfy the trace preserving condition

$$ \sum_{k=0}^{r-1} A_k^\dagger A_k = I $$

where $I$ is the identity matrix. The matrices $A_k$ are sometimes called Kraus or noise operators.

The cirq.kraus returns a tuple of numpy arrays, one for each of the Kraus operators:

cirq.kraus(cirq.DepolarizingChannel(p=0.3))
(array([[0.83666003, 0.        ],
        [0.        , 0.83666003]]),
 array([[0.        +0.j, 0.31622777+0.j],
        [0.31622777+0.j, 0.        +0.j]]),
 array([[0.+0.j        , 0.-0.31622777j],
        [0.+0.31622777j, 0.+0.j        ]]),
 array([[ 0.31622777+0.j,  0.        +0.j],
        [ 0.        +0.j, -0.31622777+0.j]]))

In case the operator does not implement SupportsKraus, but it does implement SupportsMixture, the *kraus protocol will generate the Kraus operators based on the *mixture representation.

$$ ((p_0, U_0),(p_1, U_1),\ldots,(p_n, U_n)) \rightarrow (\sqrt{p_0}U_0, \sqrt{p_1}U_1, \ldots, \sqrt{p_n}U_n) $$

Thus for example ((0.25, X), (0.75, I)) -> (0.5 X, sqrt(0.75) I):

cirq.kraus(cirq.X.with_probability(0.25))
(array([[0. +0.j, 0.5+0.j],
        [0.5+0.j, 0. +0.j]]),
 array([[0.8660254, 0.       ],
        [0.       , 0.8660254]]))

In the simplest case of a unitary operator, cirq.kraus returns a one-element tuple with the same unitary as returned by cirq.unitary:

print(cirq.kraus(cirq.Y))
print(cirq.unitary(cirq.Y))
print(cirq.has_kraus(cirq.Y))
(array([[0.+0.j, 0.-1.j],
       [0.+1.j, 0.+0.j]]),)
[[0.+0.j 0.-1.j]
 [0.+1.j 0.+0.j]]
True