Realistic Noise Models¶
YAQS ships a library of physically motivated jump operators—relaxation (lowering), excitation (raising), single-qubit Pauli channels, and nearest-neighbor crosstalk (crosstalk_xx, crosstalk_zz, …)—that you assemble into a NoiseModel.
For hardware with static disorder (calibration drift, fabrication spread), each process strength can be a distribution instead of a fixed float. YAQS samples one concrete strength per process when run() starts; all trajectories in that run share the same sampled disorder. The realized model is stored on noise_model.
This page shows:
A typical multi-channel noise model for an analog chain.
Log-normal disorder on strengths (recommended when rates span orders of magnitude) and other built-in distributions.
How sampled disorder changes open-system dynamics compared to a median-strength baseline.
Custom jump operators via an explicit
matrix(not only built-in library names).
1. Built-in noise processes¶
Each process is a dictionary with name, sites, and strength. YAQS fills in the operator matrix (or per-site factors for long-range crosstalk) from NoiseLibrary.
1from mqt.yaqs import NoiseModel
2
3L = 4
4processes = [
5 {"name": "lowering", "sites": [i], "strength": 0.05} for i in range(L)
6] + [
7 {"name": "pauli_z", "sites": [i], "strength": 0.02} for i in range(L)
8] + [
9 {"name": "crosstalk_xx", "sites": [i, i + 1], "strength": 0.01} for i in range(L - 1)
10]
11
12noise_model = NoiseModel(processes)
2. Log-normal disorder on strengths¶
When calibration rates vary across devices or qubits, strengths often span several orders of magnitude. A log-normal distribution is usually more realistic than a symmetric Gaussian on the rate itself.
Replace a scalar strength with a dict. For log-normal sampling, mean and std are the parameters of the underlying normal distribution on \(\log\gamma\):
1bell_curve_strength = {"distribution": "lognormal", "mean": -2.3, "std": 0.5}
2
3disordered_processes = [
4 {
5 "name": "pauli_z",
6 "sites": [i],
7 "strength": bell_curve_strength,
8 }
9 for i in range(L)
10]
11
12disordered_model = NoiseModel(disordered_processes)
Other supported distributions:
|
Parameters |
Use when |
|---|---|---|
|
|
Default choice for positive rates spanning magnitudes ( |
|
|
Symmetric spread around a target rate; negatives are clamped to |
|
|
Same shape as normal but sampled only for non-negative strengths. |
Sample many independent disorder realizations and plot the bell curve on a log scale:
1import matplotlib.pyplot as plt
2import matplotlib.ticker as mticker
3import numpy as np
4from scipy import stats
5
6rng = np.random.default_rng(0)
7samples = [disordered_model.sample(rng=rng).processes[0]["strength"] for _ in range(5000)]
8
9mu = bell_curve_strength["mean"]
10sigma = bell_curve_strength["std"]
11x = np.logspace(np.log10(min(samples)), np.log10(max(samples)), 200)
12pdf = stats.lognorm.pdf(x, s=sigma, scale=np.exp(mu))
13
14fig, ax = plt.subplots(figsize=(7, 3.8), layout="constrained")
15ax.hist(samples, bins=40, density=True, alpha=0.7, color="tab:blue", label="sampled strengths")
16ax.plot(x, pdf, color="black", lw=1.5, label="log-normal pdf")
17ax.set_xscale("log")
18
19# Sparse decade ticks with plain decimal labels (avoids crowded sci-notation on log axes)
20lo, hi = float(min(samples)), float(max(samples))
21tick_decades = np.arange(int(np.floor(np.log10(lo))), int(np.ceil(np.log10(hi))) + 1)
22tick_candidates = np.concatenate([np.array([1, 2, 5]) * 10.0**e for e in tick_decades])
23ticks = tick_candidates[(tick_candidates >= lo * 0.9) & (tick_candidates <= hi * 1.1)]
24if len(ticks) > 6:
25 ticks = ticks[np.linspace(0, len(ticks) - 1, 6, dtype=int)]
26ax.set_xticks(ticks)
27ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:g}"))
28ax.xaxis.set_minor_locator(mticker.NullLocator())
29
30ax.set_xlabel("sampled dephasing strength")
31ax.set_ylabel("density")
32ax.set_title("Log-normal disorder (median ≈ {:.3f})".format(np.exp(mu)))
33ax.legend()
34ax.grid(alpha=0.3, which="both")
35plt.show()
3. Disorder in an analog simulation¶
We evolve a short Ising chain from a Néel product state and compare:
Baseline: every site uses the log-normal median \(\exp(\text{mean})\) as a fixed strength.
Disordered: strengths are drawn from the log-normal once at the start of each run.
Ensemble band: several independent disorder draws (different
random_seed) to show typical spread.
1from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State
2
3# Wider log-normal spread for a visible disorder effect in dynamics
4dyn_strength = {"distribution": "lognormal", "mean": -0.7, "std": 1.0}
5dyn_disordered = NoiseModel([
6 {"name": "pauli_z", "sites": [i], "strength": dyn_strength} for i in range(L)
7])
8
9hamiltonian = Hamiltonian.ising(length=L, J=1.0, g=0.5)
10state = State(L, initial="Neel")
11z_obs = Observable("z", sites=0)
12
13sim_params = AnalogSimParams(
14 observables=[z_obs],
15 elapsed_time=8.0,
16 dt=0.1,
17 num_traj=32,
18 max_bond_dim=24,
19 random_seed=7,
20)
21
22median_strength = float(np.exp(dyn_strength["mean"]))
23baseline_model = NoiseModel([
24 {"name": "pauli_z", "sites": [i], "strength": median_strength} for i in range(L)
25])
26
27sim = Simulator(show_progress=False)
28result_baseline = sim.run(state, hamiltonian, sim_params, baseline_model)
29result_disordered = sim.run(state, hamiltonian, sim_params, dyn_disordered)
30
31# Ensemble of disorder realizations for a shaded band (keep small for doc build time)
32ensemble_curves = []
33for seed in range(8, 12):
34 params_i = AnalogSimParams(
35 observables=[z_obs],
36 elapsed_time=8.0,
37 dt=0.1,
38 num_traj=16,
39 max_bond_dim=24,
40 random_seed=seed,
41 )
42 res_i = sim.run(state, hamiltonian, params_i, dyn_disordered)
43 ensemble_curves.append(res_i.expectation_values[0])
44ensemble_curves = np.asarray(ensemble_curves)
1import matplotlib.pyplot as plt
2import matplotlib.ticker as mticker
3
4times = sim_params.times
5baseline_curve = result_baseline.expectation_values[0]
6disordered_curve = result_disordered.expectation_values[0]
7
8fig, ax = plt.subplots(figsize=(7, 4), layout="constrained")
9ax.fill_between(
10 times,
11 ensemble_curves.min(axis=0),
12 ensemble_curves.max(axis=0),
13 color="tab:orange",
14 alpha=0.25,
15 label="disordered ensemble (4 seeds)",
16)
17ax.plot(times, baseline_curve, label="fixed median strength", color="black", linestyle="--", lw=2)
18ax.plot(times, disordered_curve, label="one disordered sample", color="tab:orange", lw=1.5)
19ax.set_xlabel("time")
20ax.set_ylabel(r"$\langle Z_0 \rangle$")
21ax.set_title("Log-normal static disorder shifts open-system decay")
22ax.xaxis.set_major_locator(mticker.MaxNLocator(6))
23ax.legend()
24ax.grid(alpha=0.3)
25plt.show()
Re-running with the same random_seed reproduces the same sampled strengths and trajectory-averaged curve. Leave random_seed=None for fresh disorder draws in production Monte Carlo studies.
4. Disorder on a noisy circuit¶
The same distribution syntax works in digital simulation. Below, bit-flip rates on each qubit follow independent log-normal draws; one sample is drawn per Simulator.run call.
1from mqt.yaqs import Observable, StrongSimParams
2from mqt.yaqs.core.libraries.circuit_library import create_ising_circuit
3
4num_qubits = 3
5circuit = create_ising_circuit(L=num_qubits, J=1.0, g=0.5, dt=0.1, timesteps=5)
6
7circuit_noise = NoiseModel([
8 {
9 "name": "pauli_x",
10 "sites": [i],
11 "strength": {"distribution": "lognormal", "mean": -3.0, "std": 0.4},
12 }
13 for i in range(num_qubits)
14])
15
16circuit_params = StrongSimParams(
17 observables=[Observable("z", site) for site in range(num_qubits)],
18 num_traj=32,
19 max_bond_dim=8,
20 random_seed=11,
21)
22
23circuit_result = sim.run(State(num_qubits, initial="zeros"), circuit, circuit_params, circuit_noise)
5. Long-range crosstalk¶
Non-adjacent pairs use the longrange_crosstalk_{ab} naming convention; YAQS attaches per-site Pauli factors automatically:
1lr_model = NoiseModel([
2 {"name": "longrange_crosstalk_xy", "sites": [0, 2], "strength": 0.05},
3])
4sampled = lr_model.sample(rng=0)
6. Custom jump operators¶
Every noise process is a dictionary. Besides the built-in NoiseLibrary names (lowering, pauli_x, crosstalk_xx, …), you can supply your own operator as a NumPy array:
Key |
Required |
Description |
|---|---|---|
|
yes |
Label for the process. When |
|
yes |
Site indices the jump acts on (one site for single-qubit channels). |
|
yes |
Rate \(\gamma\) in Lindblad form; YAQS uses jump operators \(L_k = \sqrt{\gamma}\,L\). |
|
no |
Local operator \(L\) as a |
YAQS does not check complete positivity; supply physically meaningful jump operators. The same matrix override works for scheduled jumps (see Scheduled Jumps) and for all backends—TJM (mps), MCWF (vector), Lindblad (density_matrix), and noisy circuits.
Amplitude damping with an explicit \(\sigma_-\)¶
The built-in lowering operator is \(\sigma_- = |0\rangle\langle 1|\). You can pass the same matrix explicitly and mix custom and library processes in one model:
1import numpy as np
2
3sigma_minus = np.array([[0, 1], [0, 0]], dtype=complex)
4
5custom_model = NoiseModel([
6 {"name": "t1_explicit", "sites": [0], "strength": 0.1, "matrix": sigma_minus},
7 {"name": "pauli_z", "sites": [1], "strength": 0.05},
8])
Run a short analog simulation—the custom operator is used wherever NoiseModel.processes is consumed:
1from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State
2
3L2 = 2
4hamiltonian = Hamiltonian.ising(length=L2, J=1.0, g=0.5)
5state = State(L2, initial="basis", basis_string="10")
6
7sim_params = AnalogSimParams(
8 observables=[Observable("z", sites=0), Observable("z", sites=1)],
9 elapsed_time=1.0,
10 dt=0.1,
11 num_traj=32,
12 max_bond_dim=8,
13 random_seed=3,
14)
15
16result = Simulator(show_progress=False).run(state, hamiltonian, sim_params, custom_model)
For \(d>2\) local Hilbert spaces (e.g. transmon leakage), pass a d×d matrix matching the site’s physical dimension—see Transmon-Resonator Chain Emulation.