Analytical and Monte Carlo noise¶
Sweep a depolarizing rate on a 3-qubit GHZ state and compare Maestro’s two fast ways to estimate expectation values under Pauli noise.
import numpy as np
import maestro
from maestro.circuits import QuantumCircuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
rates = np.linspace(0, 0.1, 11)
analytical, monte_carlo = [], []
for p in rates:
noise = maestro.NoiseModel()
noise.set_all_depolarizing(num_qubits=3, p=p)
exact = maestro.noisy_estimate(qc, ["XXX"], noise)
sampled = maestro.noisy_estimate_montecarlo(
qc, ["XXX"], noise, noise_realizations=500, noise_seed=7
)
analytical.append(exact["expectation_values"][0])
monte_carlo.append(sampled["expectation_values"][0])
for p, a, m in list(zip(rates, analytical, monte_carlo))[::5]:
print(f"p={p:.2f} analytical {a:.3f} Monte Carlo {m:.3f}")
p=0.00 analytical 1.000 Monte Carlo 1.000
p=0.05 analytical 0.813 Monte Carlo 0.728
p=0.10 analytical 0.651 Monte Carlo 0.516
noisy_estimateruns the circuit once without noise and damps each expectation value analytically. It costs nothing extra.noisy_estimate_montecarloinjects random Pauli errors after every gate and averages over many noisy runs, so noise builds up with circuit depth.
The Monte Carlo curve falls faster. A depolarizing channel with probability p
scales a Pauli expectation value by 1 − 4p/3 each time it hits a qubit the
observable acts on. The analytical mode applies that factor once per qubit, three
times for XXX. Monte Carlo applies it after every gate, and five gate
locations here touch a qubit the observable depends on:
p = rates[-1]
print(f"analytical {analytical[-1]:.3f} (1 - 4p/3)^3 = {(1 - 4 * p / 3) ** 3:.3f}")
print(f"Monte Carlo {monte_carlo[-1]:.3f} (1 - 4p/3)^5 = {(1 - 4 * p / 3) ** 5:.3f}")
analytical 0.651 (1 - 4p/3)^3 = 0.651
Monte Carlo 0.516 (1 - 4p/3)^5 = 0.489
Use the analytical mode to screen many circuits quickly, and Monte Carlo when the depth of the circuit matters. Noise and error correction covers every noise mode.