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
This doc assumes you have already read the noisy simulation tutorial.
Cirq provides several built-in tools for representing noise at multiple levels:
- Channels, to insert as individual noisy operators
cirq.NoiseModel
s, for applying noise to entire circuitscirq.MeasurementGate
parameters, for changing measurements results
This doc describes these options and the types of real-world noise they can be used to represent.
Channels
Errors in hardware can be broadly separated into two categories: coherent and incoherent.
Coherent errors apply a reversible (but unknown) transformation, such as making every \(Z\) gate instead behave as \(Z^{1.01}\). This can be represented by inserting gates into the intended circuit.
Incoherent errors cause decoherence of the quantum state, and are irreversible as a result. This is equivalent to applying an operation with some probability \(0 < P < 1\), and can be represented with Cirq "channels". ops/common_channels.py
defines channels for some of the most common incoherent errors, which are described below.
Bit flip
cirq.BitFlipChannel
(or cirq.bit_flip
) is equivalent to applying cirq.X
with a given probability. This channel is best used to represent state-agnostic bit flip errors in the body of a circuit.
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(
cirq.bit_flip(p=0.2).on(q0),
cirq.measure(q0, key='result')
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
print(result.histogram(key='result'))
Counter({0: 795, 1: 205})
For bit flips which depend on the qubit state, see Amplitude damping.
For measurement error that doesn't affect the quantum state, see Invert mask.
Amplitude damping
cirq.AmplitudeDampingChannel
(or cirq.amplitude_damp
) performs a \(|1\rangle \rightarrow |0\rangle\) transformation with some probability gamma
, leaving the existing \(|0\rangle\) state alone. This channel is best used to represent an idealized form of energy dissipation, where qubits decay from \(|1\rangle\) to \(|0\rangle\).
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(
cirq.X(q0),
cirq.amplitude_damp(gamma=0.2).on(q0),
cirq.measure(q0, key='result')
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
print(result.histogram(key='result'))
Counter({1: 795, 0: 205})
For state-agnostic bit flips, see Bit flip.
Generalized amplitude damping
cirq.GeneralizedAmplitudeDampingChannel
(or cirq.generalized_amplitude_damp
) is a generalized version of AmplitudeDampingChannel
. It represent a more realistic bidirectional energy dissipation, in which qubits experience not only decay but also spontaneous excitation. In this channel, gamma
represents the probability of energy transfer (excitation OR decay) and a new parameter p
gives the probability that the environment is excited.
This is equivalent to excitation with probability (1-p) * gamma
and decay with probability p * gamma
.
q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
cirq.X(q1),
cirq.generalized_amplitude_damp(gamma=0.2, p=0.2).on_each(q0, q1),
cirq.measure(q0, key='result_0'),
cirq.measure(q1, key='result_1'),
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
print("Starting in |0):", result.histogram(key='result_0'))
print("Starting in |1):", result.histogram(key='result_1'))
Starting in |0): Counter({0: 824, 1: 176}) Starting in |1): Counter({1: 956, 0: 44})
Phase flip or damping
cirq.PhaseFlipChannel
(or cirq.phase_flip
) is equivalent to applying cirq.Z
with a given probability p
. This channel is best used to represent state-agnostic phase flip errors in the body of a circuit.
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(
cirq.H(q0),
cirq.phase_flip(p=0.2).on(q0),
cirq.H(q0),
cirq.measure(q0, key='result')
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
print("Phase flip:", result.histogram(key='result'))
Phase flip: Counter({0: 795, 1: 205})
cirq.PhaseDampingChannel
(or cirq.phase_damp
) is a different way of expressing the same behavior: for any given value of p
, PhaseFlipChannel(p=p)
is equivalent to PhaseDampingChannel(gamma=(1-(2*p-1)**2))
.
q0 = cirq.LineQubit(0)
# Convert p=0.2 to gamma
p = 0.2
gamma = 1 - (2 * p - 1) ** 2
print(f"{gamma=}")
circuit = cirq.Circuit(
cirq.H(q0),
cirq.phase_damp(gamma=gamma).on(q0),
cirq.H(q0),
cirq.measure(q0, key='result')
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
print("Phase damp:", result.histogram(key='result'))
gamma=0.64 Phase damp: Counter({0: 777, 1: 223})
Note that the results differ despite the same seed and equivalent circuits. This is due to the channels having different operators, which interact differently with Cirq's RNG.
Depolarization
cirq.DepolarizingChannel
(or cirq.depolarize
) is equivalent to applying a randomly-selected Pauli operator to the target qubits. The identity is applied with probability 1-p
; all other Pauli operators have an equal probability p / (4**n-1)
of being selected. This channel is best used for representing uniformly-distributed decoherence of the target qubit(s) across all Pauli channels.
q0, q1, q2 = cirq.LineQubit.range(3)
circuit = cirq.Circuit(
cirq.H(q0), # initialize X basis
cirq.H(q1), # initialize Y basis
cirq.S(q1),
cirq.depolarize(p=0.2).on_each(q0, q1, q2),
cirq.H(q0), # return to Z-basis
cirq.S(q1) ** -1,
cirq.H(q1),
cirq.measure(q0, key='result_0'),
cirq.measure(q1, key='result_1'),
cirq.measure(q2, key='result_2'),
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
# All basis states are equally affected.
print("X basis:", result.histogram(key='result_0'))
print("Y basis:", result.histogram(key='result_1'))
print("Z basis:", result.histogram(key='result_2'))
X basis: Counter({0: 872, 1: 128}) Y basis: Counter({0: 862, 1: 138}) Z basis: Counter({0: 892, 1: 108})
For noise in just the X or Z channels, see Bit flip or Phase flip respectively.
Asymmetric depolarization
cirq.AsymmetricDepolarizingChannel
(or cirq.asymmetric_depolarize
) is a generalized version of DepolarizingChannel
which accepts separate probabilities for X, Y, and Z error. It is best used instead of DepolarizingChannel
when there is a known, nontrivial discrepancy between the different Pauli error modes.
q0, q1, q2 = cirq.LineQubit.range(3)
asym_depol = cirq.asymmetric_depolarize(p_x=0, p_y=0.05, p_z=0.2)
circuit = cirq.Circuit(
cirq.H(q0), # initialize X basis
cirq.H(q1), # initialize Y basis
cirq.S(q1),
asym_depol.on_each(q0, q1, q2),
cirq.H(q0), # return to Z-basis
cirq.S(q1) ** -1,
cirq.H(q1),
cirq.measure(q0, key='result_0'),
cirq.measure(q1, key='result_1'),
cirq.measure(q2, key='result_2'),
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
# Basis states are only affected by error in other bases.
print("X basis:", result.histogram(key='result_0'))
print("Y basis:", result.histogram(key='result_1'))
print("Z basis:", result.histogram(key='result_2'))
X basis: Counter({0: 764, 1: 236}) Y basis: Counter({0: 800, 1: 200}) Z basis: Counter({0: 950, 1: 50})
Reset
cirq.Reset
forces a qubit into the \(|0\rangle\) state. This is not a noise channel, but rather a hardware operation which commonly consists of measuring the qubit and applying X
as needed to return it to the \(|0\rangle\) state.
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(
cirq.bit_flip(p=0.2).on(q0),
cirq.reset(q0),
cirq.measure(q0, key='result')
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
print(result.histogram(key='result'))
Counter({0: 1000})
Custom channels
cirq.MixedUnitaryChannel
(in ops/mixed_unitary_channel.py
) is a customizable channel which can represent any probabilistic mixture of unitary operators. It accepts an optional measurement key to capture which operator was selected.
q0 = cirq.LineQubit(0)
# equivalent to cirq.bit_flip(p=0.2)
my_channel = cirq.MixedUnitaryChannel(
[(0.8, cirq.unitary(cirq.I)), (0.2, cirq.unitary(cirq.X))],
key='op_num',
)
circuit = cirq.Circuit(
my_channel.on(q0),
cirq.measure(q0, key='result')
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=20)
# `op_num` and `result` are always equal.
print(result)
op_num=00001000001000000001 result=00001000001000000001
cirq.KrausChannel
(in ops/kraus_channel.py
) is similar, but supports non-unitary operators.
import numpy as np
q0 = cirq.LineQubit(0)
# equivalent to cirq.amplitude_damp(gamma=0.2)
gamma = 0.2
my_channel = cirq.KrausChannel(
[
np.array([[0, np.sqrt(gamma)], [0, 0]]), # decay |1) -> |0)
np.array([[1, 0], [0, np.sqrt(1-gamma)]]), # stay in |1)
],
key='op_num',
)
circuit = cirq.Circuit(
cirq.X(q0),
my_channel.on(q0),
cirq.measure(q0, key='result')
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=20)
# `op_num` and `result` are always equal.
print(result)
op_num=11111110011100111011 result=11111110011100111011
In general, prefer one of the other built-in channels if your use case supports it, as those channels can occasionally be optimized in ways that do not generalize to these channels.
Prefer MixedUnitaryChannel
if your channel has a mix-of-unitaries description, as it can be simulated more efficiently than KrausChannel
.
NoiseModels
Built-in cirq.NoiseModel
types do not have a shared home like channels, but a couple of commonly-used types are listed here. For more complex experiments, it is often useful to define your own NoiseModel
subclasses; refer to devices/noise_model.py
to learn more.
Constant noise
cirq.ConstantQubitNoiseModel
(in devices/noise_model.py
) is a simple model which will insert the given gate after every operation in the target circuit. When "trivially converting" gates to NoiseModel
s, this is the model that is used, but it isn't particularly representative of any real-world noise.
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(
cirq.I(q0),
cirq.measure(q0, key='result_0'),
cirq.measure(q0, key='result_1'),
)
# Applies noise after every gate, even measurements.
noisy_circuit = circuit.with_noise(cirq.X)
print(noisy_circuit)
result = cirq.Simulator(seed=0).run(noisy_circuit, repetitions=20)
print("First measure:", result.histogram(key='result_0'))
print("Second measure:", result.histogram(key='result_1'))
0: ───I───X[<virtual>]───M('result_0')───X[<virtual>]───M('result_1')───X[<virtual>]─── First measure: Counter({1: 20}) Second measure: Counter({0: 20})
Avoid using this model except for simple tests, as different gates (particularly cirq.MeasurementGate
) usually have different error.
Insertion noise
cirq.devices.InsertionNoiseModel
(in devices/insertion_noise_model.py
) inspects the circuit for operations matching user-specified identifiers, and inserts the corresponding noise operations after matching operations. This noise model is useful for applying specific noise to specific gates - for example, adding different depolarizing error to 1- and 2-qubit gates.
from cirq.devices import InsertionNoiseModel
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(
cirq.I(q0),
cirq.X(q0),
cirq.measure(q0, key='result'),
)
# Apply bitflip noise after each X gate.
target_op = cirq.OpIdentifier(cirq.XPowGate, q0)
insert_op = cirq.bit_flip(p=0.2).on(q0)
noise_model = InsertionNoiseModel(
ops_added={target_op: insert_op},
require_physical_tag=False, # For use outside calibration-to-noise
)
noisy_circuit = circuit.with_noise(noise_model)
print(noisy_circuit)
result = cirq.Simulator(seed=0).run(noisy_circuit, repetitions=1000)
print(result.histogram(key='result'))
0: ───I───X───BF(0.2)───M('result')─── Counter({1: 795, 0: 205})
InsertionNoiseModel
is primarily used in the calibration-to-noise pipeline, but can be used elsewhere by setting require_physical_tag=False
, as seen above.
Measurement parameters
cirq.MeasurementGate
provides parameters for error which occurs in the classical measurement step instead of in the quantum state, which can be useful for accelerating simulations.
Invert mask
The invert_mask
field is a simple list of booleans indicating bits to flip in the final output. This can represent simple bitflip error in measurement, or a correction for that error.
q0 = cirq.LineQubit(0)
circuit = cirq.Circuit(
cirq.X(q0),
cirq.measure(q0, key='result', invert_mask=[True])
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
print(result.histogram(key='result'))
Counter({0: 1000})
Confusion map
The confusion_map
field maps qubit tuples to confusion matrices for those qubits. The confusion matrix for two qubits is:
\[\begin{bmatrix} Pr(00|00) & Pr(01|00) & Pr(10|00) & Pr(11|00) \\ Pr(00|01) & Pr(01|01) & Pr(10|01) & Pr(11|01) \\ Pr(00|10) & Pr(01|10) & Pr(10|10) & Pr(11|10) \\ Pr(00|11) & Pr(01|11) & Pr(10|11) & Pr(11|11) \end{bmatrix}\]
where Pr(ij|pq)
is the probability of observing ij
if state pq
was prepared; a 2**n
-square confusion matrix can be provided for any grouping of N qubits.
import numpy as np
q0 = cirq.LineQubit(0)
# 10% chance to report |0) as |1), 20% chance to report |1) as |0).
cmap = {(0,): np.array([[0.9, 0.1], [0.2, 0.8]])}
circuit = cirq.Circuit(
cirq.X(q0),
cirq.measure(q0, key='result', confusion_map=cmap)
)
result = cirq.Simulator(seed=0).run(circuit, repetitions=1000)
print(result.histogram(key='result'))
Counter({1: 787, 0: 213})
This can be used for representing more complex errors in measurement, including probabilistic error on individual qubits and correlated error across multiple qubits.