Noise Characterization in Cirq
Characterize noise in Cirq circuits using cross-entropy benchmarking and depolarizing error models. Validate against Google Quantum AI hardware specifications.
Circuit diagrams
Overview
Before running algorithms on real quantum hardware, you need to understand the noise affecting your device. Every physical qubit experiences errors from unwanted interactions with its environment, imperfect control pulses, and crosstalk with neighboring qubits. “Noise characterization” means building a mathematical model that predicts how these errors degrade your circuit results.
This model serves two practical purposes. First, it lets you simulate realistic (noisy) circuits on a classical computer before spending time and money on expensive hardware access. You can prototype error mitigation strategies, estimate whether your algorithm will produce useful results at a given circuit depth, and identify which qubits or gates are the weakest links. Second, it tells you where to focus error mitigation effort. If your two-qubit gates have 10x higher error rates than your single-qubit gates (which is typical), you know to minimize two-qubit gate count rather than obsessing over single-qubit rotations.
Cirq provides tools for both building noise models and running the characterization experiments that parameterize them. This tutorial walks through the full workflow: constructing different noise channel types, running cross-entropy benchmarking (XEB) to measure gate fidelity, fitting noise parameters from experimental data, and validating against Google Quantum AI hardware specifications.
Setting Up Cirq
pip install cirq cirq-google scipy matplotlib
import cirq
import numpy as np
import matplotlib.pyplot as plt
Types of Quantum Noise
Not all noise is the same. Understanding the type of noise present on your hardware is a prerequisite to modeling and mitigating it. Each noise channel captures a different physical mechanism, and the right model depends on your hardware platform and circuit structure.
Depolarizing Noise
Depolarizing noise applies a random Pauli operator (X, Y, or Z) with equal probability. It is the simplest noise model and a good first approximation for most hardware, because it treats all error directions symmetrically.
qubit = cirq.LineQubit(0)
# Depolarizing channel with total error probability p = 0.01
# With probability (1 - p), nothing happens (identity).
# With probability p/3 each, an X, Y, or Z error occurs.
depolarizing_channel = cirq.depolarize(p=0.01)
# Apply to a circuit via a noise model
circuit = cirq.Circuit([cirq.H(qubit)])
print(cirq.Circuit(depolarizing_channel.on(qubit)))
Amplitude Damping (T1 Noise)
Amplitude damping models energy relaxation: the qubit decays from |1> to |0> over time. The T1 time characterizes how quickly this happens. This noise channel is relevant for gates with long durations, because the qubit has more time to relax during the operation.
# Amplitude damping with decay probability gamma
# gamma relates to gate duration t and T1 time: gamma = 1 - exp(-t / T1)
gamma = 0.02 # probability of |1> decaying to |0> during one gate
amplitude_damping = cirq.amplitude_damp(gamma=gamma)
circuit_ad = cirq.Circuit([
cirq.X(qubit), # Prepare |1>
amplitude_damping.on(qubit), # Apply T1 decay
])
sim = cirq.DensityMatrixSimulator()
result = sim.simulate(circuit_ad)
print("After amplitude damping:")
print(result.final_density_matrix)
Dephasing (T2 Noise)
Dephasing destroys phase coherence without causing energy loss. The qubit forgets the relative phase between |0> and |1>, which collapses superpositions toward a classical mixture. The T2 time characterizes this process. Dephasing is particularly important for deep circuits where qubits sit idle while other operations execute.
# Phase damping with dephasing probability gamma
# gamma relates to gate duration t and T2 time: gamma = 1 - exp(-t / T2)
gamma_phase = 0.03
phase_damping = cirq.phase_damp(gamma=gamma_phase)
circuit_pd = cirq.Circuit([
cirq.H(qubit), # Create superposition (sensitive to dephasing)
phase_damping.on(qubit),
])
result_pd = sim.simulate(circuit_pd)
print("After phase damping:")
print(result_pd.final_density_matrix)
Coherent Errors
Coherent errors are systematic over-rotations or under-rotations in gate angles. Unlike the stochastic noise channels above, coherent errors are deterministic and can accumulate rather than average out. A gate that consistently over-rotates by 0.01 radians will be off by 1 radian after 100 applications.
# Model a coherent Z-rotation error of 0.01 radians after each gate
coherent_error_angle = 0.01 # radians of systematic over-rotation
class CoherentErrorModel(cirq.NoiseModel):
def __init__(self, angle: float):
self.angle = angle
def noisy_operation(self, op):
# Add a small unwanted Z-rotation after every single-qubit gate
if len(op.qubits) == 1:
return [op, cirq.rz(self.angle).on(op.qubits[0])]
return op
coherent_sim = cirq.DensityMatrixSimulator(noise=CoherentErrorModel(coherent_error_angle))
circuit_coherent = cirq.Circuit([cirq.H(qubit)] * 50)
result_coherent = coherent_sim.simulate(circuit_coherent)
print("After 50 gates with coherent error:")
print(result_coherent.final_density_matrix)
Crosstalk
Crosstalk occurs when a gate on one qubit unexpectedly affects neighboring qubits. This is particularly problematic for two-qubit gates, where the strong coupling needed for the intended interaction can leak to adjacent qubits on the chip.
q0, q1, q2 = cirq.LineQubit.range(3)
class CrosstalkModel(cirq.NoiseModel):
def __init__(self, crosstalk_p: float):
self.crosstalk_p = crosstalk_p
def noisy_operation(self, op):
# When a CZ acts on (q0, q1), add depolarizing noise on neighbor q2
if isinstance(op.gate, cirq.CZPowGate) and len(op.qubits) == 2:
neighbor = q2 # assume q2 is an adjacent spectator qubit
return [op, cirq.depolarize(p=self.crosstalk_p).on(neighbor)]
return op
crosstalk_sim = cirq.DensityMatrixSimulator(noise=CrosstalkModel(crosstalk_p=0.005))
circuit_xtalk = cirq.Circuit([
cirq.H(q0),
cirq.H(q2), # Spectator qubit in superposition (sensitive to crosstalk)
cirq.CZ(q0, q1),
cirq.measure(q0, q1, q2, key="m"),
])
result_xtalk = crosstalk_sim.run(circuit_xtalk, repetitions=1000)
print(result_xtalk.histogram(key="m"))
Depolarizing Noise Models
The depolarizing channel is the workhorse of noise modeling. It captures the intuition that a noisy gate randomly “kicks” the qubit state in an unpredictable direction. Physically, with probability p/4 each, the qubit either experiences no error (identity), a bit flip (X), a phase flip (Z), or both simultaneously (Y). The parameter p is the total error probability per gate.
For superconducting qubits on current hardware, typical values are p = 0.001 to 0.005 for single-qubit gates and p = 0.005 to 0.02 for two-qubit gates. These numbers come from randomized benchmarking experiments published by Google, IBM, and other hardware teams.
qubit = cirq.LineQubit(0)
# Single-qubit depolarizing noise
noise_model = cirq.ConstantQubitNoiseModel(cirq.depolarize(p=0.01))
noisy_sim = cirq.DensityMatrixSimulator(noise=noise_model)
circuit = cirq.Circuit([
cirq.H(qubit),
cirq.measure(qubit, key="m"),
])
result = noisy_sim.simulate(circuit)
print(result.final_density_matrix)
The density matrix output reveals the effect of noise. An ideal Hadamard on |0> produces a pure state with off-diagonal elements of magnitude 0.5. Depolarizing noise shrinks these off-diagonal elements, mixing the state toward the maximally mixed state (identity/2). The larger the p value, the faster this mixing occurs.
Two-Qubit Depolarizing Noise
Real hardware has stronger two-qubit gate errors. This asymmetry is fundamental: two-qubit gates require coupling qubits together, which introduces additional error channels that single-qubit gates do not face. Model this with a two-qubit depolarizing channel applied after each two-qubit gate.
q0, q1 = cirq.LineQubit.range(2)
# Build a noisy model that inserts depolarizing noise after each 2-qubit gate
class TwoQubitDepolarizingNoise(cirq.NoiseModel):
def __init__(self, p: float):
self.p = p
def noisy_operation(self, op):
if len(op.qubits) == 2:
return [op, cirq.depolarize(p=self.p).on_each(*op.qubits)]
return op
circuit_2q = cirq.Circuit([
cirq.H(q0),
cirq.CNOT(q0, q1),
cirq.measure(q0, q1, key="m"),
])
noisy_sim = cirq.DensityMatrixSimulator(noise=TwoQubitDepolarizingNoise(p=0.005))
result = noisy_sim.run(circuit_2q, repetitions=100)
print(result)
Building a Realistic Noise Model
Real hardware has different noise rates for different qubits and different gate types. A uniform noise model (same error rate everywhere) is a useful starting point, but it misses important structure. Some qubits sit near the edge of the chip and have fewer neighbors, leading to lower crosstalk. Others may have fabrication defects that make them noisier. Building a non-uniform noise model captures these variations.
Per-Qubit Error Rates
Assign different depolarizing error rates to individual qubits based on calibration data.
class PerQubitNoiseModel(cirq.NoiseModel):
"""Noise model with different error rates per qubit and per gate type."""
def __init__(
self,
single_qubit_errors: dict,
two_qubit_error: float,
default_single_error: float = 0.002,
):
self.single_qubit_errors = single_qubit_errors
self.two_qubit_error = two_qubit_error
self.default_single_error = default_single_error
def noisy_operation(self, op):
if len(op.qubits) == 2:
# Two-qubit gates get their own (higher) error rate
return [op, cirq.depolarize(p=self.two_qubit_error).on_each(*op.qubits)]
elif len(op.qubits) == 1:
qubit = op.qubits[0]
p = self.single_qubit_errors.get(qubit, self.default_single_error)
return [op, cirq.depolarize(p=p).on(qubit)]
return op
# Non-uniform noise model: qubit 1 is noisier than qubit 0
per_qubit_error = {
cirq.LineQubit(0): 0.001,
cirq.LineQubit(1): 0.003, # qubit 1 is noisier
cirq.LineQubit(2): 0.002,
}
realistic_noise = PerQubitNoiseModel(
single_qubit_errors=per_qubit_error,
two_qubit_error=0.01, # all two-qubit gates share this rate
)
qubits = cirq.LineQubit.range(3)
test_circuit = cirq.Circuit([
cirq.H.on_each(*qubits),
cirq.CNOT(qubits[0], qubits[1]),
cirq.CNOT(qubits[1], qubits[2]),
cirq.measure(*qubits, key="m"),
])
realistic_sim = cirq.DensityMatrixSimulator(noise=realistic_noise)
result = realistic_sim.run(test_circuit, repetitions=1000)
print(result.histogram(key="m"))
Combining Multiple Noise Channels
Real qubits experience depolarizing noise, amplitude damping, and dephasing simultaneously. You can layer these channels in a single noise model.
class CombinedNoiseModel(cirq.NoiseModel):
"""Applies depolarizing noise, T1 decay, and T2 dephasing after each gate."""
def __init__(self, p_depol: float, gamma_ad: float, gamma_pd: float):
self.p_depol = p_depol
self.gamma_ad = gamma_ad
self.gamma_pd = gamma_pd
def noisy_operation(self, op):
if cirq.is_measurement(op):
return op
noise_ops = [op]
for qubit in op.qubits:
noise_ops.append(cirq.depolarize(p=self.p_depol).on(qubit))
noise_ops.append(cirq.amplitude_damp(gamma=self.gamma_ad).on(qubit))
noise_ops.append(cirq.phase_damp(gamma=self.gamma_pd).on(qubit))
return noise_ops
combined_noise = CombinedNoiseModel(p_depol=0.002, gamma_ad=0.001, gamma_pd=0.003)
combined_sim = cirq.DensityMatrixSimulator(noise=combined_noise)
q = cirq.LineQubit(0)
deep_circuit = cirq.Circuit([cirq.H(q)] * 20 + [cirq.measure(q, key="m")])
result_combined = combined_sim.run(deep_circuit, repetitions=1000)
print(result_combined.histogram(key="m"))
Cross-Entropy Benchmarking (XEB)
Cross-Entropy Benchmarking is the gold standard for measuring gate fidelity on quantum hardware. It works by exploiting a statistical property of random quantum circuits.
The core idea is straightforward. Generate a random circuit, compute the ideal output probability distribution using a classical simulator, then run the same circuit on the noisy device and observe which bitstrings come out. If the device is perfect, it produces the high-probability bitstrings more often than the low-probability ones. If the device is completely noisy (fully depolarized), it produces all bitstrings with equal frequency, regardless of the ideal distribution.
The linear XEB fidelity quantifies this:
F_XEB = d * <P_ideal> - 1
Here d is the Hilbert space dimension (2^n for n qubits), and <P_ideal> is the average ideal probability of the bitstrings that the noisy device actually outputs. Intuitively, F_XEB measures how often the noisy device outputs the “heavy” (high-probability) bitstrings from the ideal distribution.
What do the fidelity values mean?
- F_XEB = 1.0 in expectation: When averaged over many Haar-random circuits, F_XEB converges to 1.0 for a perfect device. For any individual random circuit, F_XEB can exceed 1.0 because
d * dot(p, p) - 1is circuit-dependent; values above 1 are normal and expected for single circuits. - F_XEB = 0.0 in expectation: When averaged over many circuits, a completely depolarized device outputs a uniform distribution and F_XEB converges to 0.0.
- The range [0, 1] holds in expectation: Over many random circuits, the average F_XEB lies in [0, 1] and higher values indicate lower noise. Do not interpret a single-circuit F_XEB > 1.0 as a calibration error.
Why use random circuits for this measurement? Structured circuits (like repeated Hadamard gates or periodic patterns) can have noise that cancels or averages out in misleading ways. Random circuits average over all possible error configurations, giving a robust estimate of the average gate fidelity that is not sensitive to special symmetries.
def random_xeb_circuit(qubits, depth, seed=42):
"""Generate a random circuit for XEB."""
rng = np.random.default_rng(seed)
gates = [cirq.X**0.5, cirq.Y**0.5, cirq.T]
circuit = cirq.Circuit()
for _ in range(depth):
layer = [rng.choice(gates)(q) for q in qubits]
circuit.append(layer)
circuit.append(cirq.CZ(qubits[0], qubits[1]))
return circuit
qubits = cirq.LineQubit.range(2)
xeb_circuit = random_xeb_circuit(qubits, depth=10)
# Ideal simulation
ideal_sim = cirq.Simulator()
ideal_result = ideal_sim.simulate(xeb_circuit)
ideal_probs = np.abs(ideal_result.final_state_vector) ** 2
# Noisy simulation
noisy_sim2 = cirq.DensityMatrixSimulator(
noise=cirq.ConstantQubitNoiseModel(cirq.depolarize(p=0.005))
)
noisy_result = noisy_sim2.simulate(xeb_circuit)
noisy_probs = np.diag(noisy_result.final_density_matrix).real
# Linear XEB fidelity calculation
n_states = len(ideal_probs)
xeb_fidelity = n_states * np.dot(ideal_probs, noisy_probs) - 1
print(f"XEB fidelity score: {xeb_fidelity:.4f}")
An XEB fidelity near 1.0 in expectation (averaged over many random circuits) is ideal. For this single circuit the value may differ from 1.0 in either direction. Typical Google Sycamore two-qubit gate fidelities reach 0.995 or above when averaged over many circuits.
Running XEB with Multiple Random Circuits
A single random circuit gives a noisy estimate of the fidelity. For reliable results, average over many random circuits at each depth.
def xeb_fidelity_estimate(qubits, depth, n_circuits=50, noise_model=None):
"""Estimate XEB fidelity at a given depth, averaged over multiple random circuits."""
ideal_sim = cirq.Simulator()
noisy_sim = cirq.DensityMatrixSimulator(noise=noise_model)
n_states = 2 ** len(qubits)
fidelities = []
for seed in range(n_circuits):
circuit = random_xeb_circuit(qubits, depth, seed=seed)
ideal_result = ideal_sim.simulate(circuit)
ideal_probs = np.abs(ideal_result.final_state_vector) ** 2
noisy_result = noisy_sim.simulate(circuit)
noisy_probs = np.diag(noisy_result.final_density_matrix).real
f = n_states * np.dot(ideal_probs, noisy_probs) - 1
fidelities.append(f)
return np.mean(fidelities), np.std(fidelities) / np.sqrt(n_circuits)
qubits = cirq.LineQubit.range(2)
noise = cirq.ConstantQubitNoiseModel(cirq.depolarize(p=0.005))
mean_f, std_f = xeb_fidelity_estimate(qubits, depth=10, n_circuits=50, noise_model=noise)
print(f"XEB fidelity at depth 10: {mean_f:.4f} +/- {std_f:.4f}")
Fitting a Noise Model from XEB Data
Run XEB circuits at multiple depths and fit an exponential decay to extract per-cycle fidelity.
For depolarizing noise, the XEB fidelity decays exponentially with circuit depth:
F(d) = A * r^d
The parameter r is the per-cycle fidelity. Each “cycle” consists of one layer of single-qubit gates followed by one two-qubit gate, so r captures the combined error of all gates in one cycle. For a two-qubit depolarizing channel, r relates to the depolarizing error rate by:
r = 1 - 5p/4
The factor of 5/4 comes from the structure of the two-qubit depolarizing channel (there are 15 non-identity two-qubit Pauli operators out of 16 total, but the formula simplifies for the standard parameterization). The fitted r lets you compute the average gate error without needing full quantum process tomography, which requires exponentially more measurements.
The amplitude A accounts for state preparation and measurement (SPAM) errors, which affect all depths equally. By fitting the exponential, you separate SPAM errors (captured by A) from gate errors (captured by r).
depths = [1, 2, 5, 10, 20]
n_circuits_per_depth = 50
fidelities = []
for d in depths:
fids_at_depth = []
for seed in range(n_circuits_per_depth):
circ = random_xeb_circuit(qubits, depth=d, seed=seed)
ideal = ideal_sim.simulate(circ)
ip = np.abs(ideal.final_state_vector) ** 2
noisy = noisy_sim2.simulate(circ)
np_ = np.diag(noisy.final_density_matrix).real
f = len(ip) * np.dot(ip, np_) - 1
fids_at_depth.append(f)
fidelities.append(np.mean(fids_at_depth))
# Fit exponential: F(d) = A * r^d
from scipy.optimize import curve_fit
def exp_decay(d, A, r):
return A * r ** np.array(d)
popt, pcov = curve_fit(exp_decay, depths, fidelities, p0=[1.0, 0.99])
print(f"Per-cycle fidelity (r): {popt[1]:.5f}")
print(f"SPAM factor (A): {popt[0]:.5f}")
print(f"Equivalent depolarizing error (p): {(1 - popt[1]) * 4/5:.5f}")
# Plot the fit
plt.figure(figsize=(8, 5))
plt.scatter(depths, fidelities, label="Measured XEB fidelity", zorder=5)
d_smooth = np.linspace(0, max(depths), 100)
plt.plot(d_smooth, exp_decay(d_smooth, *popt), "r--", label=f"Fit: A={popt[0]:.3f}, r={popt[1]:.4f}")
plt.xlabel("Circuit depth (cycles)")
plt.ylabel("XEB fidelity")
plt.title("XEB Fidelity Decay")
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig("xeb_fidelity_decay.png", dpi=150)
plt.show()
Comparing Noise Models
Different noise channels produce different fidelity decay signatures. You can use XEB data to distinguish between depolarizing, amplitude damping, and dephasing noise by comparing the quality of each fit.
from scipy.optimize import curve_fit
# Simulate XEB under three different noise models
noise_models = {
"Depolarizing (p=0.01)": cirq.ConstantQubitNoiseModel(cirq.depolarize(p=0.01)),
"Amplitude Damping (gamma=0.01)": cirq.ConstantQubitNoiseModel(cirq.amplitude_damp(gamma=0.01)),
"Phase Damping (gamma=0.01)": cirq.ConstantQubitNoiseModel(cirq.phase_damp(gamma=0.01)),
}
depths = [1, 2, 3, 5, 8, 10, 15, 20]
qubits = cirq.LineQubit.range(2)
ideal_sim = cirq.Simulator()
plt.figure(figsize=(10, 6))
n_circuits_per_depth = 50
for label, noise in noise_models.items():
noisy_sim = cirq.DensityMatrixSimulator(noise=noise)
fids = []
for d in depths:
fids_at_depth = []
for seed in range(n_circuits_per_depth):
circ = random_xeb_circuit(qubits, depth=d, seed=seed)
ideal = ideal_sim.simulate(circ)
ip = np.abs(ideal.final_state_vector) ** 2
noisy = noisy_sim.simulate(circ)
np_ = np.diag(noisy.final_density_matrix).real
f = len(ip) * np.dot(ip, np_) - 1
fids_at_depth.append(f)
fids.append(np.mean(fids_at_depth))
# Fit exponential decay to each
try:
popt, pcov = curve_fit(exp_decay, depths, fids, p0=[1.0, 0.99])
residuals = np.array(fids) - exp_decay(np.array(depths), *popt)
rmse = np.sqrt(np.mean(residuals**2))
plt.plot(depths, fids, "o-", label=f"{label} (r={popt[1]:.4f}, RMSE={rmse:.4f})")
except RuntimeError:
plt.plot(depths, fids, "o-", label=f"{label} (fit failed)")
plt.xlabel("Circuit depth (cycles)")
plt.ylabel("XEB fidelity")
plt.title("Fidelity Decay Under Different Noise Models")
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig("noise_model_comparison.png", dpi=150)
plt.show()
If the depolarizing fit has large residuals (high RMSE), the actual noise may have more structure than a simple depolarizing model captures. Large residuals at short depths suggest coherent errors (which decay quadratically, not exponentially, at early times). Systematic deviations at long depths suggest T1 or T2 effects, where the fidelity floor depends on the equilibrium state rather than decaying to zero.
Comparing to Google Hardware Specifications
Google Quantum AI publishes calibration data for Sycamore. You can load device-specific noise via cirq_google:
import cirq_google
# Load a saved calibration (requires access to Google Cloud Quantum Computing Service)
# calibration = cirq_google.get_engine_calibration(processor_id="rainbow")
# noise_props = cirq_google.noise_properties_from_calibration(calibration)
# noise_model = cirq_google.NoiseModelFromGoogleNoiseProperties(noise_props)
print("Device-specific noise models available via cirq_google.noise_properties_from_calibration")
When you have access to the Google Quantum Computing Service, the calibration data includes per-qubit T1 times, per-gate error rates, and readout error probabilities. The noise_properties_from_calibration function converts these into a Cirq noise model that you can plug directly into a DensityMatrixSimulator. This lets you simulate your specific algorithm under realistic device conditions before using real hardware time.
Common Mistakes
Using Too Few XEB Circuits or Shots
The linear XEB fidelity estimate has high variance from individual random circuits. Some circuits happen to have output distributions that are easier or harder to distinguish from uniform noise. You need at least 50 random circuits per depth point to get a reliable average, and at least 1000 shots per circuit when sampling from real hardware. With fewer circuits, your fidelity estimate can fluctuate by 0.1 or more, which makes it impossible to distinguish between noise models.
Confusing Symmetric and Asymmetric Noise
Depolarizing noise is symmetric: it is equally likely to apply X, Y, or Z errors. T1 (amplitude damping) noise is asymmetric: only |1> decays to |0>, not the reverse. This distinction matters when you simulate circuits that spend most of their time near |0> versus |1>. A depolarizing model predicts equal error rates for both preparations, while a T1 model correctly predicts that |1> preparations decay faster. If your fitted depolarizing model works well for some circuits but poorly for others, asymmetric T1 noise may be the cause.
Confusing Per-Gate and Per-Cycle Fidelity
Each XEB cycle contains one layer of single-qubit gates AND one two-qubit gate. The per-cycle fidelity r from the exponential fit reflects the combined error of all gates in the cycle. If you have single-qubit gate error p1 = 0.001 and two-qubit gate error p2 = 0.01, the per-cycle error is roughly 2*p1 + p2 = 0.012 (for a two-qubit circuit), not just p2. Reporting the per-cycle fidelity as the “two-qubit gate fidelity” overstates the two-qubit gate error.
Applying Single-Qubit Noise to Two-Qubit Gates
A single-qubit depolarizing channel has 3 non-trivial Pauli errors (X, Y, Z). A two-qubit depolarizing channel has 15 non-trivial Pauli errors (all two-qubit Paulis except II). These require different parameterizations. Applying cirq.depolarize(p=0.01).on_each(q0, q1) applies independent single-qubit noise to each qubit, which is not the same as correlated two-qubit depolarizing noise. For correlated two-qubit errors, use cirq.depolarize(p=0.01, n_qubits=2).on(q0, q1).
q0, q1 = cirq.LineQubit.range(2)
# Independent single-qubit depolarizing (does NOT capture correlated errors)
independent_noise = cirq.depolarize(p=0.01).on_each(q0, q1)
# Correlated two-qubit depolarizing (captures all 15 two-qubit Pauli errors)
correlated_noise = cirq.depolarize(p=0.01, n_qubits=2).on(q0, q1)
print("Independent noise:", independent_noise)
print("Correlated noise:", correlated_noise)
Summary
Start with a depolarizing noise model as a first approximation, since it captures the dominant error behavior with a single parameter. Use cross-entropy benchmarking to measure the actual per-cycle fidelity at multiple circuit depths, then fit an exponential decay to extract the depolarizing error rate. Compare your fitted model against amplitude damping and dephasing alternatives by checking the fit residuals. If the depolarizing model fits well, use it for circuit simulation and error mitigation prototyping. If it does not, layer in T1 and T2 channels to build a more accurate model. Cirq’s cirq_google module bridges the gap between simulation and real device characterization by converting hardware calibration data directly into noise models.
Was this tutorial helpful?