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

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_ 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 _channel_ protocol)
  • Pauli basis (cirq.PauliSum)
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(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.channel(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, '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, 'key', ())(0)

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

kraus_ops = cirq.channel(measurement)
print(f"Kraus operators of {measurement} are:", *kraus_ops, sep="\n\n")
Kraus operators of cirq.MeasurementGate(1, '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: 0
Resultant state:
 [1. 0.]
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: 0
Resultant state:
 [[1. 0.]
 [0. 0.]]

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.channel(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.

Observables

Cirq supports observables which are Pauli strings or linear combinations of Pauli strings. Such objects can be used to compute expectation values.

Pauli strings

Pauli products or strings are supported via cirq.PauliString. For example, the Pauli string $Z_0 Z_1$ (where subscripts denote qubit indices) can be represented as follows.

# Qubit register
qreg = cirq.NamedQubit.range(2, prefix="q")

# PauliString Z_0 Z_1
zz = cirq.PauliString(cirq.Z(q) for q in qreg)
print(zz)
Z(q0)*Z(q1)

The matrix of a Pauli string can be returned via:

zz.matrix()
array([[ 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, -1.+0.j, -0.+0.j],
       [ 0.+0.j, -0.+0.j, -0.+0.j,  1.-0.j]])

Pauli strings can also have arbitrary coefficients.

new = (1.0 - 0.1j) * zz
print(new)
(1-0.1j)*Z(q0)*Z(q1)

Pauli sums

A cirq.PauliSum is a linear combination of cirq.PauliStrings and represents a Hamiltonian (or general observable) in the Pauli basis. To represent the observable $O = 1.5 Z_0 Z_1 - 0.7 X_0 X_1$, we can first define the $X_0 X_1$ Pauli string:

xx = cirq.PauliString(cirq.X(q) for q in qreg)
print(xx)
X(q0)*X(q1)

Then form the linear combination.

psum = 1.5 * zz - 0.7 * xx
print(psum)
1.500*Z(q0)*Z(q1)-0.700*X(q0)*X(q1)

Like Pauli strings, we can get the matrix of a cirq.PauliSum:

psum.matrix()
array([[ 1.5+0.j,  0. +0.j,  0. +0.j, -0.7+0.j],
       [ 0. +0.j, -1.5+0.j, -0.7+0.j,  0. +0.j],
       [ 0. +0.j, -0.7+0.j, -1.5+0.j,  0. +0.j],
       [-0.7+0.j,  0. +0.j,  0. +0.j,  1.5+0.j]])

Expectation values

Given a state $\rho$ that is prepared by a circuit, expectation values $\text{Tr} [ \rho O ]$ where $O$ is an observable can be computed as follows. First, an example circuit:

circuit = cirq.Circuit(cirq.ops.H.on_each(qreg))
print(circuit)
q0: ───H───

q1: ───H───

The pattern for computing $\text{Tr} [ \rho O ]$ is shown below.

# Define a PauliSumCollector.
collector = cirq.PauliSumCollector(circuit, psum, samples_per_term=10_000)

# Provide a sampler. See also: collector.collect(...).
collector.collect_async(sampler=cirq.Simulator())

# Estimate the observable.
energy = collector.estimated_energy()
print("Energy:", energy)
Energy: 0.0
/home/kbuilder/.local/lib/python3.6/site-packages/ipykernel_launcher.py:5: RuntimeWarning: coroutine 'Collector.collect_async' was never awaited
  """

Note that this method uses sampling with a number of samples given by samples_per_term.

Expectation values can also be computed from NumPy array representations of quantum states. For example, given a state vector we can do the following.

# Get the state vector.
psi = circuit.final_state_vector()

# Compute the expectation value.
energy = psum.expectation_from_state_vector(
    state_vector=psi, qubit_map={q: i for i, q in enumerate(qreg)}
)
print("Energy:", energy.real)
Energy: -0.7000000000000003

And given a density matrix, we can compute the expectation via:

# Get the density matrix.
dsim = cirq.DensityMatrixSimulator()
rho = dsim.simulate(circuit).final_density_matrix

# Compute the expectation value.
energy = psum.expectation_from_density_matrix(
    state=rho, qubit_map={q: i for i, q in enumerate(qreg)}
)
print("Energy:", energy.real)
Energy: -0.6999999165534972

Pauli expansions

The function cirq.pauli_expansion can return Pauli basis representations of certain objects. For example, given the circuit $H \otimes H$ from above:

circuit

The Pauli expansion can be obtained via:

psum = cirq.pauli_expansion(circuit)
print(psum)
0.500*XX+0.500*XZ+0.500*ZX+0.500*ZZ

Generally, the argument to cirq.pauli_expansion must define the _pauli_expansion_ method or have a small unitary representation. The argument default can be provided to return a default value if no expansion can be computed.