Operators and observables

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.")
    import cirq

import numpy as np
import sympy.parsing.sympy_parser as sympy_parser

This guide is directed at those already familiar with quantum operations (operators) and observables who want to know how to use them in Cirq. The following table shows an overview of operators.

Operator Cirq representation Guides Examples
Unitary operators Any class implementing the _unitary_ and _has_unitary_ protocol Protocols, Gates and operations, Custom gates cirq.Gate
cirq.X(qubit)
cirq.CNOT(q0, q1)
cirq.MatrixGate.on(qubit)
cirq.Circuit (if it only contains unitary operations)
Measurements cirq.measure and cirq.MeasurementGate Gates and operations cirq.measure(cirq.LineQubit(0))
Quantum channels
  • Kraus operators (any class implementing the _kraus_ and _has_kraus_ protocol)
  • Unitary mixtures (any class implementing the _mixture_ and _has_mixture_ protocol)
Protocols cirq.DepolarizingChannel(p=0.2)(q0)
cirq.X.with_probability(0.5)

Cirq also supports observables on qubits that can be used to calculate expectation values on a given state.

Operators

Quantum operations (or just operators) include unitary gates, measurements, and noisy channels. Operators that act on a given set of qubits implement cirq.Operation which supports the Kraus operator representation

\[ \rho \mapsto \sum_{k} A_k \rho A_k^\dagger . \]

Here, \(\sum_{k} A_k^\dagger A_k = I\) and \(\rho\) is a quantum state. Operators are defined in the cirq.ops module.

Unitary operators

Standard unitary operators used in quantum information can be found in cirq.ops, for example Pauli-\(X\) as shown below.

qubit = cirq.LineQubit(0)
unitary_operation = cirq.ops.X.on(qubit)  # cirq.X can also be used for cirq.ops.X
print(unitary_operation)
X(q(0))

Cirq makes a distinction between gates (independent of qubits) and operations (gates acting on qubits). Thus cirq.X is a gate where cirq.X.on(qubit) is an operation. See the guide on gates for more details and additional common unitaries defined in Cirq.

Every cirq.Operation supports the cirq.channel protocol which returns its Kraus operators. (Read more about protocols in Cirq.)

kraus_ops = cirq.kraus(unitary_operation)
print(f"Kraus operators of {unitary_operation.gate} are:", *kraus_ops, sep="\n")
Kraus operators of X are:
[[0.+0.j 1.+0.j]
 [1.+0.j 0.+0.j]]

Unitary operators also support the cirq.unitary protocol.

unitary = cirq.unitary(cirq.ops.X)
print(f"Unitary of {unitary_operation.gate} is:\n", unitary)
Unitary of X is:
 [[0.+0.j 1.+0.j]
 [1.+0.j 0.+0.j]]

Unitary gates can be raised to powers, for example to implement a \(\sqrt{X}\) operation.

sqrt_not = cirq.X ** (1 / 2)
print(cirq.unitary(sqrt_not))
[[0.5+0.5j 0.5-0.5j]
 [0.5-0.5j 0.5+0.5j]]

Any gate can be controlled via cirq.ControlledGate as follows.

controlled_hadamard = cirq.ControlledGate(sub_gate=cirq.H, num_controls=1)
print(cirq.unitary(controlled_hadamard).round(3))
[[ 1.   +0.j  0.   +0.j  0.   +0.j  0.   +0.j]
 [ 0.   +0.j  1.   +0.j  0.   +0.j  0.   +0.j]
 [ 0.   +0.j  0.   +0.j  0.707+0.j  0.707+0.j]
 [ 0.   +0.j  0.   +0.j  0.707+0.j -0.707+0.j]]

Custom gates can be defined as described in this guide. Some common subroutines which consist of several operations are pre-defined - e.g., cirq.qft returns the operations to implement the quantum Fourier transform.

Measurements

Cirq supports measurements in the computational basis.

measurement = cirq.MeasurementGate(num_qubits=1, key="key")
print("Measurement:", measurement)
Measurement: cirq.MeasurementGate(1, cirq.MeasurementKey(name='key'), ())

The key can be used to identify results of measurements when simulating circuits. A measurement gate acting on a qubit forms an operation.

measurement_operation = measurement.on(qubit)
print(measurement_operation)
cirq.MeasurementGate(1, cirq.MeasurementKey(name='key'), ())(q(0))

Again measurement operations implement cirq.Operation so the cirq.channel protocol can be used to get the Kraus operators.

kraus_ops = cirq.kraus(measurement)
print(f"Kraus operators of {measurement} are:", *kraus_ops, sep="\n\n")
Kraus operators of cirq.MeasurementGate(1, cirq.MeasurementKey(name='key'), ()) are:

[[1. 0.]
 [0. 0.]]

[[0. 0.]
 [0. 1.]]

The functions cirq.measure_state_vector and cirq.measure_density_matrix can be used to perform computational basis measurements on state vectors and density matrices, respectively, represented by NumPy arrays.

psi = np.ones(shape=(2,)) / np.sqrt(2)
print("Wavefunction:\n", psi.round(3))
Wavefunction:
 [0.707 0.707]
results, psi_prime = cirq.measure_state_vector(psi, indices=[0])

print("Measured:", results[0])
print("Resultant state:\n", psi_prime)
Measured: 1
Resultant state:
 [0. 1.]
rho = np.ones(shape=(2, 2)) / 2.0
print("State:\n", rho)
State:
 [[0.5 0.5]
 [0.5 0.5]]
measurements, rho_prime = cirq.measure_density_matrix(rho, indices=[0])

print("Measured:", measurements[0])
print("Resultant state:\n", rho_prime)
Measured: 1
Resultant state:
 [[0. 0.]
 [0. 1.]]

These functions do not modify the input state (psi or rho) unless the optional argument out is provided as the input state.

Noisy channels

Like common unitary gates, Cirq defines many common noisy channels, for example the depolarizing channel below.

depo_channel = cirq.DepolarizingChannel(p=0.01, n_qubits=1)
print(depo_channel)
depolarize(p=0.01)

Just like unitary gates and measurements, noisy channels implement cirq.Operation, and we can always use cirq.channel to get the Kraus operators.

kraus_ops = cirq.kraus(depo_channel)
print(f"Kraus operators of {depo_channel} are:", *[op.round(2) for op in kraus_ops], sep="\n\n")
Kraus operators of depolarize(p=0.01) are:

[[0.99 0.  ]
 [0.   0.99]]

[[0.  +0.j 0.06+0.j]
 [0.06+0.j 0.  +0.j]]

[[0.+0.j   0.-0.06j]
 [0.+0.06j 0.+0.j  ]]

[[ 0.06+0.j  0.  +0.j]
 [ 0.  +0.j -0.06+0.j]]

Some channels can be written

\[ \rho \mapsto \sum_k p_k U_k \rho U_k ^\dagger \]

where real numbers \(p_k\) form a probability distribution and \(U_k\) are unitary. Such a probabilistic mixture of unitaries supports the cirq.mixture protocol which returns \(p_k\) and \(U_k\). An example is shown below for the bit-flip channel \(\rho \mapsto (1 - p) \rho + p X \rho X\).

bit_flip = cirq.bit_flip(p=0.05)
probs, unitaries = cirq.mixture(bit_flip)

for prob, unitary in cirq.mixture(bit_flip):
    print(f"With probability {prob}, apply \n{unitary}\n")
With probability 0.95, apply 
[[1. 0.]
 [0. 1.]]

With probability 0.05, apply 
[[0.+0.j 1.+0.j]
 [1.+0.j 0.+0.j]]

Custom noisy channels can be defined as described in this guide.

In circuits

Any cirq.Operation (pre-defined or user-defined) can be placed in a cirq.Circuit. An example with a unitary, noisy channel, and measurement is shown below.

circuit = cirq.Circuit(
    cirq.H(qubit),
    cirq.depolarize(p=0.01).on(qubit),
    cirq.measure(qubit)
)
print(circuit)
0: ───H───D(0.01)───M───

The general input to the circuit constructor is a cirq.OP_TREE, i.e., an operation or nested collection of operations. Circuits can be manipulated as described in the circuits guide and simulated as described in the simulation guide.

Alternate representations

In addition to the above representations for operators. Cirq also supports some more non-standard representations as well. To convert a set of kraus operators to a choi representation you can do:

depo_channel = cirq.DepolarizingChannel(p=0.01, n_qubits=1)
kraus_rep = cirq.kraus(depo_channel)
print(kraus_rep)
(array([[0.99498744, 0.        ],
       [0.        , 0.99498744]]), array([[0.        +0.j, 0.05773503+0.j],
       [0.05773503+0.j, 0.        +0.j]]), array([[0.+0.j        , 0.-0.05773503j],
       [0.+0.05773503j, 0.+0.j        ]]), array([[ 0.05773503+0.j,  0.        +0.j],
       [ 0.        +0.j, -0.05773503+0.j]]))
choi_rep = cirq.kraus_to_choi(kraus_rep)
print(choi_rep)
[[0.99333333+0.j 0.        +0.j 0.        +0.j 0.98666667+0.j]
 [0.        +0.j 0.00666667+0.j 0.        +0.j 0.        +0.j]
 [0.        +0.j 0.        +0.j 0.00666667+0.j 0.        +0.j]
 [0.98666667+0.j 0.        +0.j 0.        +0.j 0.99333333+0.j]]

And to get the superoperator representation you can do:

super_rep = cirq.kraus_to_superoperator(kraus_rep)
print(super_rep)
[[0.99333333+0.j 0.        +0.j 0.        +0.j 0.00666667+0.j]
 [0.        +0.j 0.98666667+0.j 0.        +0.j 0.        +0.j]
 [0.        +0.j 0.        +0.j 0.98666667+0.j 0.        +0.j]
 [0.00666667+0.j 0.        +0.j 0.        +0.j 0.99333333+0.j]]