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:

  1. A typical multi-channel noise model for an analog chain.

  2. Log-normal disorder on strengths (recommended when rates span orders of magnitude) and other built-in distributions.

  3. How sampled disorder changes open-system dynamics compared to a median-strength baseline.

  4. 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:

distribution

Parameters

Use when

"lognormal"

mean, std

Default choice for positive rates spanning magnitudes (mean/std on \(\log\gamma\)).

"normal"

mean, std

Symmetric spread around a target rate; negatives are clamped to 0.

"truncated_normal"

mean, std

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()
../_images/8004cae5a168ef315f83dbb18fc62411c4c06f42cc7b755c943ea0e78f504484.svg

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

name

yes

Label for the process. When matrix is omitted, must match a NoiseLibrary entry. When matrix is provided, any string is fine.

sites

yes

Site indices the jump acts on (one site for single-qubit channels).

strength

yes

Rate \(\gamma\) in Lindblad form; YAQS uses jump operators \(L_k = \sqrt{\gamma}\,L\).

matrix

no

Local operator \(L\) as a d×d array (d=2 for qubits). If omitted, YAQS looks up name in NoiseLibrary.

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.