From 984ff617a89f682aae77646fb8ef5a6d93abd658 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:00:15 +0200 Subject: [PATCH 01/30] HTML and Markdown now use same cached simulations to speed up docs --- docs/conf.py | 16 +++++++ tests/docs/test_build.py | 98 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 tests/docs/test_build.py diff --git a/docs/conf.py b/docs/conf.py index b3fbed286..1b8ce0a9c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,6 +24,17 @@ ROOT = Path(__file__).parent.parent.resolve() +# Limit docs kernels and child builds unless the runner supplies a budget. +for name in ( + "MKL_NUM_THREADS", + "NUMBA_NUM_THREADS", + "NUMEXPR_NUM_THREADS", + "OMP_NUM_THREADS", + "OPENBLAS_NUM_THREADS", +): + os.environ.setdefault(name, "1") +os.environ.setdefault("YAQS_MAX_WORKERS", "2") + # Keep matplotlib/font cache writable and local during docs builds. os.environ.setdefault("MPLCONFIGDIR", str(ROOT / "docs" / "_build" / ".mplconfig")) @@ -104,6 +115,11 @@ nb_execution_mode = "cache" nb_execution_raise_on_error = True +nb_execution_cache_path = str(ROOT / "docs" / "_build" / ".jupyter_cache") + +# Reuse HTML doctrees and notebook outputs when generating the Markdown files. +llms_txt_build_parallel = False +llms_txt_full_build = True class CDAStyle(UnsrtStyle): diff --git a/tests/docs/test_build.py b/tests/docs/test_build.py new file mode 100644 index 000000000..4ec82195e --- /dev/null +++ b/tests/docs/test_build.py @@ -0,0 +1,98 @@ +# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Integration tests for executable documentation builds.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + + +def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: + """Both documentation formats retain outputs from one capped notebook run.""" + pytest.importorskip("sphinx") + pytest.importorskip("myst_nb") + pytest.importorskip("sphinx_llm.txt") + pytest.importorskip("pybtex") + + source = tmp_path / "docs" + source.mkdir() + configuration = Path(__file__).parents[2] / "docs" / "conf.py" + (source / "conf.py").write_text( + configuration.read_text() + + '\nextensions = ["myst_nb", "sphinx_llm.txt"]\n' + + 'html_theme = "basic"\n' + + "html_theme_options = {}\n" + + "html_static_path = []\n" + + "html_css_files = []\n" + + "templates_path = []\n" + ) + records = tmp_path / "executions.jsonl" + (source / "index.md").write_text( + "---\nfile_format: mystnb\nkernelspec:\n name: python3\nlanguage_info:\n name: python\n---\n\n" + "# Executable documentation\n\n" + "```{code-cell} ipython3\n" + "import json\n" + "import os\n" + "from pathlib import Path\n" + "import numba\n" + "import numpy as np\n" + "from threadpoolctl import threadpool_info\n" + "from mqt.yaqs import Simulator\n\n" + "np.eye(4) @ np.eye(4)\n" + "record = {\n" + ' "thread_limits": {name: os.environ[name] for name in (\n' + ' "MKL_NUM_THREADS", "NUMBA_NUM_THREADS", "NUMEXPR_NUM_THREADS",\n' + ' "OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS",\n' + " )},\n" + ' "numba_threads": numba.get_num_threads(),\n' + ' "blas_threads": [pool["num_threads"] for pool in threadpool_info()],\n' + ' "workers": Simulator(show_progress=False).max_workers,\n' + "}\n" + f"with Path({str(records)!r}).open('a') as stream:\n" + " stream.write(json.dumps(record) + '\\n')\n" + 'print("NOTEBOOK_RESULT_" + str(6 * 7))\n' + "```\n" + ) + output = tmp_path / "_build" / "html" + environment = os.environ.copy() + for name in ( + "MKL_NUM_THREADS", + "NUMBA_NUM_THREADS", + "NUMEXPR_NUM_THREADS", + "OMP_NUM_THREADS", + "OPENBLAS_NUM_THREADS", + "YAQS_MAX_WORKERS", + "PYTEST_XDIST_WORKER", + ): + environment.pop(name, None) + environment["IPYTHONDIR"] = str(tmp_path / ".ipython") + environment["JUPYTER_RUNTIME_DIR"] = str(tmp_path / ".jupyter") + completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - sys.executable is trusted. + [sys.executable, "-m", "sphinx", "-W", "-T", "-b", "html", str(source), str(output)], + env=environment, + check=False, + capture_output=True, + text=True, + timeout=60, + ) + + assert completed.returncode == 0, completed.stdout + completed.stderr + runs = [json.loads(line) for line in records.read_text().splitlines()] + assert len(runs) == 1 + assert all(limit == "1" for limit in runs[0]["thread_limits"].values()) + assert runs[0]["numba_threads"] == 1 + assert 1 <= runs[0]["workers"] <= 2 + assert all(threads == 1 for threads in runs[0]["blas_threads"]) + for artifact in ("index.html", "index.html.md", "llms-full.txt"): + assert "NOTEBOOK_RESULT_42" in (output / artifact).read_text() From d4817fab62a7b5fff531e41537302ce7f90b6454 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:00:31 +0200 Subject: [PATCH 02/30] set PyTorch to CPU only in docs --- .readthedocs.yaml | 15 ++++++++------- noxfile.py | 13 ++++--------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index cb4b694bd..8c8f66239 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,8 +1,5 @@ version: 2 -formats: - - htmlzip - sphinx: configuration: docs/conf.py @@ -10,11 +7,15 @@ build: os: ubuntu-24.04 tools: python: "3.14" + jobs: + install: + - uv pip install --python "$READTHEDOCS_VIRTUALENV_PATH/bin/python" --group docs --torch-backend cpu --exact -e '.[qasm3,torch]' python: install: - method: uv - command: sync - groups: - - docs - extras: all + command: pip + path: . + extras: + - qasm3 + - torch diff --git a/noxfile.py b/noxfile.py index 4fe90c02a..a61dc8695 100755 --- a/noxfile.py +++ b/noxfile.py @@ -245,10 +245,11 @@ def docs(session: nox.Session) -> None: args, posargs = parser.parse_known_args(session.posargs) serve = args.builder == "html" and session.interactive + install_args = ["--group", "docs", "--torch-backend", "cpu", "--exact", "-e", ".[qasm3,torch]"] if serve: - session.install("sphinx-autobuild") + install_args.append("sphinx-autobuild") + session.install(*install_args) - env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location} shared_args = [ "-n", # nitpicky mode "-T", # full tracebacks @@ -259,15 +260,9 @@ def docs(session: nox.Session) -> None: ] session.run( - "uv", - "run", - "--no-dev", # do not auto-install dev dependencies - "--group", - "docs", - "--all-extras", "sphinx-autobuild" if serve else "sphinx-build", *shared_args, - env=env, + env={**_CAPPED_NUMERICAL_THREADS, "YAQS_MAX_WORKERS": "2"}, ) From 2522d7af5efb1b6ced32da28b53a03b6dbe64ff1 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:57:24 +0200 Subject: [PATCH 03/30] updated quickstart --- docs/examples/quickstart.md | 565 ++++++++++++++++++------------------ 1 file changed, 287 insertions(+), 278 deletions(-) diff --git a/docs/examples/quickstart.md b/docs/examples/quickstart.md index 7ae4690e3..62e8637e8 100644 --- a/docs/examples/quickstart.md +++ b/docs/examples/quickstart.md @@ -2,345 +2,354 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true - execution_timeout: 900 + execution_timeout: 180 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Quickstart -This page runs minimal workflows end-to-end: analog and digital simulation, -equivalence checking, environmental memory (characterize from the response -matrix, then train a surrogate to predict probe density matrices under a control -sequence), and Markovian noise digital-twin fitting. Install the package first -({doc}`installation`), then copy the cells below. - -Every example in this guide uses `show_progress=False` on `Simulator`, -`MemoryCharacterizer`, and `NoiseCharacterizer` so tqdm progress bars do not -clutter the documentation; figures below each cell show the main results. +Explore the main YAQS workflows with executable examples and their results. +After {doc}`installing YAQS <../installation>`, run the cells in a notebook. For +a standalone script, put the execution code inside an +`if __name__ == "__main__":` guard; see {doc}`simulator_initialization`. -## 1. Analog simulation +The examples use `show_progress=False` to keep the documentation quiet. Omit +this argument to see progress. Times and rates use units consistent with each +Hamiltonian, with $\hbar=1$. Expand the plotting cells to reuse the figures. -Néel-initialized transverse-field Ising chain with on-site damping. Staggered -$\langle Z_i \rangle$ spreads and decays in a site-dependent way under -open-system evolution: - -```{code-cell} ipython3 +```{code-cell} python +:tags: [hide-input] import matplotlib.pyplot as plt import numpy as np +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", + "font.serif": ["STIXGeneral"], + "mathtext.fontset": "stix", + "font.size": 11, + "axes.labelsize": 11, + "axes.linewidth": 0.7, + "xtick.labelsize": 10, + "ytick.labelsize": 10, + "xtick.direction": "in", + "ytick.direction": "in", + "xtick.top": True, + "ytick.right": True, + "legend.fontsize": 9, + "legend.frameon": False, + "lines.linewidth": 1.6, + "lines.markersize": 4, + "figure.figsize": (6.6, 3.0), + "figure.constrained_layout.use": True, + "savefig.dpi": 180, +}) +``` -from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseModel, Observable, Simulator, State +## Large-scale analog dynamics -L = 5 -state = State(L, initial="Neel") -hamiltonian = Hamiltonian.ising(L, J=1.0, g=0.8) -noise_model = NoiseModel([ - {"name": "lowering", "sites": [i], "strength": 0.06} for i in range(L) -]) +Follow one excitation as it spreads through a **50-site XY spin chain**. YAQS +uses an MPS rather than storing all $2^{50}$ state amplitudes. + +```{code-cell} python +from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State +length = 50 +center = length // 2 +basis = "0" * center + "1" + "0" * (length - center - 1) +state = State(length, initial="basis", basis_string=basis) +hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) params = AnalogSimParams( - observables=[Observable("z", site) for site in range(L)], - elapsed_time=4.0, - dt=0.1, - num_traj=16, - max_bond_dim=16, - order=2, - sample_timesteps=True, + observables=[Observable("z", site) for site in range(length)], + elapsed_time=6.0, + dt=0.2, + preset="fast", ) -sim = Simulator(show_progress=False) -result = sim.run(state, hamiltonian, params, noise_model) - -heatmap = np.vstack([np.real(v) for v in result.expectation_values]) -fig, ax = plt.subplots(figsize=(6, 3.5), layout="constrained") -im = ax.imshow(heatmap, aspect="auto", extent=(0, 4.0, L, 0), vmin=-1, vmax=1, cmap="RdBu_r") -ax.set_xlabel("time") -ax.set_yticks([x - 0.5 for x in range(1, L + 1)], [str(x) for x in range(L)]) -ax.set_ylabel("site") -fig.colorbar(im, ax=ax, shrink=0.9, label=r"$\langle Z \rangle$") -ax.set_title("Staggered magnetization under damping") +simulator = Simulator(show_progress=False) +analog = simulator.run(state, hamiltonian, params) ``` -## 2. Circuit observables +```{code-cell} python +:tags: [hide-input] +from matplotlib.colors import PowerNorm -Evolve a short Trotterized Ising circuit and compare final $\langle Z_i\rangle$ -without noise and with an optional {class}`~mqt.yaqs.NoiseModel`. See -{doc}`circuit_observables` for noise sweeps, mid-circuit sampling, and gate -modes. +occupation = (1 - np.asarray(analog.expectation_values).real) / 2 +sites = np.arange(length) +fig, axes = plt.subplots(1, 2, figsize=(7.0, 3.0), width_ratios=[1.2, 1]) +image = axes[0].pcolormesh( + analog.times, sites, occupation, shading="auto", cmap="cividis", + norm=PowerNorm(0.5, vmin=0, vmax=1), rasterized=True, +) +fig.colorbar(image, ax=axes[0], label=r"$\langle n_i\rangle$", ticks=[0, 0.1, 0.5, 1]) +axes[0].set(xlabel=r"Time $t$", ylabel=r"Site $i$", xlim=(0, 6), ylim=(-0.5, length - 0.5)) +axes[0].set_title("(a) Excitation transport", loc="left", fontsize=11) +for time, color, marker in zip((2, 4, 6), ("#0072B2", "#D55E00", "#009E73"), ("o", "s", "^"), strict=True): + index = np.argmin(np.abs(analog.times - time)) + axes[1].plot(sites, occupation[:, index], color=color, marker=marker, markevery=3, label=rf"$t={time}$") +axes[1].set(xlabel=r"Site $i$", ylabel=r"Occupation $\langle n_i\rangle$", xlim=(0, length - 1), ylim=(0, 0.22)) +axes[1].set_title("(b) Spatial profiles", loc="left", fontsize=11) +axes[1].legend() +plt.show() +``` -```{code-cell} ipython3 -from mqt.yaqs import NoiseModel, Observable, DigitalSimParams -from mqt.yaqs.core.libraries.circuit_library import create_ising_circuit +The occupation $n_i=(1-Z_i)/2$ shows propagation and interference. The color +scale emphasizes small occupations; the total excitation remains one. This +low-excitation example stays inexpensive because its entanglement is limited. +For noise, larger trajectory budgets, and convergence checks, see +{doc}`analog_simulation` and {doc}`simulation_parameters`. -num_qubits = 3 -qc = create_ising_circuit(L=num_qubits, J=1.0, g=0.8, dt=0.1, timesteps=6) -circuit_state = State(num_qubits, initial="zeros") -circuit_params = DigitalSimParams( - observables=[Observable("z", site) for site in range(num_qubits)], - preset="fast", - num_traj=32, -) -noise_model = NoiseModel([ - {"name": "lowering", "sites": [site], "strength": 0.05} for site in range(num_qubits) +## Noisy circuit readout + +Prepare an eight-qubit GHZ circuit and compare ideal and damped readout with 256 +shots per run. + +```{code-cell} python +from qiskit import QuantumCircuit + +from mqt.yaqs import DigitalSimParams, NoiseModel, Simulator, State + +num_qubits = 8 +circuit = QuantumCircuit(num_qubits) +circuit.h(0) +for site in range(num_qubits - 1): + circuit.cx(site, site + 1) +circuit.measure_all() + +state = State(num_qubits, initial="zeros") +params = DigitalSimParams(shots=256, preset="fast", random_seed=7) +noise = NoiseModel([ + {"name": "lowering", "sites": [site], "strength": 0.3} for site in range(num_qubits) ]) -clean_result = sim.run(circuit_state, qc, circuit_params) -noisy_result = sim.run(State(num_qubits, initial="zeros"), qc, circuit_params, noise_model) -clean_z = np.array([float(np.real(v[0])) for v in clean_result.expectation_values]) -noisy_z = np.array([float(np.real(v[0])) for v in noisy_result.expectation_values]) - -fig, ax = plt.subplots(figsize=(5, 3), layout="constrained") -x = np.arange(num_qubits) -bar_width = 0.35 -ax.bar(x - bar_width / 2, clean_z, bar_width, label="unitary", color="0.55") -ax.bar(x + bar_width / 2, noisy_z, bar_width, label="with damping", color="C0") -ax.set_xticks(x, [rf"$\langle Z_{i}\rangle$" for i in range(num_qubits)]) -ax.set_ylim(-1.05, 1.05) -ax.set_ylabel("expectation value") -ax.set_title("Digital Ising circuit: optional open-system noise") -ax.legend(frameon=False) +simulator = Simulator(show_progress=False) +ideal = simulator.run(state, circuit, params) +damped = simulator.run(state, circuit, params, noise) ``` -## 3. Equivalence checking +```{code-cell} python +:tags: [hide-input] +excitation_number = np.arange(num_qubits + 1) +fig, ax = plt.subplots(figsize=(5.4, 2.9)) +for result, offset, color, label in ( + (ideal, -0.18, "#0072B2", "Ideal"), + (damped, 0.18, "#D55E00", "Damped"), +): + probability = np.zeros(num_qubits + 1) + for outcome, count in result.counts.items(): + probability[outcome.bit_count()] += count / params.shots + ax.bar(excitation_number + offset, probability, width=0.36, color=color, + edgecolor="white", linewidth=0.6, label=label) +ax.set(xlabel="Number of excited qubits", ylabel="Measured probability", xticks=excitation_number, ylim=(0, 0.7)) +ax.legend() +plt.show() +``` -Verify that a native GHZ circuit matches its transpiled decomposition (different -gate basis, same unitary) with {class}`~mqt.yaqs.EquivalenceChecker`: +The ideal populations lie at zero and eight excitations. Damping shifts weight +toward lower excitation numbers. This histogram summarizes readout populations; +see {doc}`circuit_shots` for bitstring counts and {doc}`circuit_observables` for +expectation values and OpenQASM input. -```{code-cell} ipython3 -from qiskit import transpile -from qiskit.circuit import QuantumCircuit +## Circuit equivalence -from mqt.yaqs import EquivalenceChecker +Verify a transpiled circuit, then measure how an extra $Z$ rotation changes its +agreement with the original. -ghz_native = QuantumCircuit(3) -ghz_native.h(0) -ghz_native.cx(0, 1) -ghz_native.cx(1, 2) +```{code-cell} python +import numpy as np +from qiskit import QuantumCircuit, transpile -ghz_transpiled = transpile( - ghz_native, - basis_gates=["rz", "sx", "x", "cx"], - optimization_level=1, -) +from mqt.yaqs import EquivalenceChecker + +circuit = QuantumCircuit(4) +circuit.h(0) +for site in range(3): + circuit.cx(site, site + 1) +decomposed = transpile(circuit, basis_gates=["rz", "sx", "x", "cx"]) + +checker = EquivalenceChecker() +print("Equivalent:", checker.check(circuit, decomposed)["equivalent"]) +angles = np.linspace(0, np.pi, 17) +overlaps = [] +for angle in angles: + perturbed = decomposed.copy() + perturbed.rz(float(angle), 0) + overlaps.append(checker.check(circuit, perturbed)["fidelity"]) +``` -checker = EquivalenceChecker(representation="mpo", threshold=1e-6) -equiv = checker.check(ghz_native, ghz_transpiled) -print(f"equivalent: {equiv['equivalent']}") -print(f"fidelity: {equiv['fidelity']:.4e}") -print(f"center-cut operator entropy: {equiv['center_cut_entanglement_entropy']:.4f}") -print(f"global operator entropy: {equiv['global_entanglement_entropy']:.4f}") - -fig, ax = plt.subplots(figsize=(4.5, 3)) -ax.semilogy(equiv["schmidt_values"], "o-") -ax.set_xlabel("Schmidt index") -ax.set_ylabel("singular value") -ax.set_title("Composed operator $W = U_1 U_2^\\dagger$") -fig.tight_layout() +```{code-cell} python +:tags: [hide-input] +fig, ax = plt.subplots(figsize=(4.6, 2.9)) +ax.plot(angles, overlaps, "o", color="#0072B2", label="YAQS") +ax.plot(angles, np.abs(np.cos(angles / 2)), "--", color="0.3", label=r"Analytic $|\cos(\theta/2)|$") +ax.set(xlabel=r"Added rotation $\theta$ (rad)", ylabel="Normalized operator overlap", + xlim=(0, np.pi), ylim=(0, 1.05), xticks=[0, np.pi / 2, np.pi], + xticklabels=["0", r"$\pi/2$", r"$\pi$"]) +ax.legend() +plt.show() ``` -For larger circuits, compiler passes, and OpenQASM inputs, see -{doc}`equivalence_checking`. +An overlap of one indicates agreement up to a global phase. The added rotation +produces a controlled difference with a known analytic overlap. See +{doc}`equivalence_checking` for noise and accuracy controls. -## 4. Characterize environmental memory +## Environmental memory -Probe a probe qubit coupled to a short chain at an interior temporal cut. The -memory spectrum and response matrix show how many independent past branches -remain visible at the cut ($S_V$). For temporal entanglement of a process tensor -($S_{PT}$), see {doc}`characterization`. +Compare the response modes of a qubit with and without coupling to a two-spin +environment, using the same probe grid. -```{code-cell} ipython3 +```{code-cell} python import numpy as np from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer -from mqt.yaqs.characterization.memory.shared.utils import make_zero_psi -length = 4 -ham = Hamiltonian.ising(length=length, J=1.0, g=1.0) -params = AnalogSimParams(dt=0.1, max_bond_dim=16, order=1) -mc = MemoryCharacterizer(show_progress=False) +params = AnalogSimParams(elapsed_time=0.5, dt=0.5, preset="fast") +characterizer = MemoryCharacterizer(show_progress=False) +memories = [] +for coupling in (0.0, 1.0): + hamiltonian = Hamiltonian.ising(3, J=coupling, g=1.0) + memories.append(characterizer.characterize( + hamiltonian, params, num_interventions=4, cut=2, preset="quick", + rng=np.random.default_rng(7), + )) +``` -cut, num_interventions = 4, 6 -result = mc.characterize( - ham, - params, - num_interventions=num_interventions, - cut=cut, - n_pasts=6, - n_futures=6, - initial_psi=make_zero_psi(length), - rng=np.random.default_rng(0), -) -sv = result.singular_values(cut) -v = result.response_matrix(cut) -# v.shape == (4 * n_futures, n_pasts), with I, X, Y, Z rows per future probe - -fig, axes = plt.subplots(1, 2, figsize=(8, 3)) -axes[0].semilogy(sv, "o-") -axes[0].set_xlabel("mode index") -axes[0].set_ylabel("singular value") -axes[0].set_title(rf"Memory spectrum: $S_V(c={cut})={result.entropy(cut):.2f}$") - -im = axes[1].imshow(np.abs(v), aspect="auto", cmap="viridis") -axes[1].set_title(rf"$|V(c)|$, $R(c)={result.modes(cut):.1f}$") -axes[1].set_xlabel("history") -axes[1].set_ylabel("future probe and response channel") -fig.colorbar(im, ax=axes[1], fraction=0.046, pad=0.04) -fig.tight_layout() +```{code-cell} python +:tags: [hide-input] +fig, axes = plt.subplots(1, 2, figsize=(6.8, 3.0)) +for memory, color, marker, label in zip( + memories, ("#D55E00", "#0072B2"), ("s", "o"), ("Uncoupled", "Coupled"), strict=True, +): + spectrum = memory.singular_values(2) + weights = spectrum**2 / np.sum(spectrum**2) + axes[0].semilogy(np.arange(1, len(weights) + 1), weights, marker=marker, color=color, label=label) +axes[0].set(xlabel="Mode index", ylabel=r"Resolved mode weight $p_k$", ylim=(1e-10, 2)) +axes[0].set_title("(a) Memory spectrum", loc="left", fontsize=11) +axes[0].legend() +image = axes[1].imshow(np.abs(memories[1].response_matrix(2)), aspect="auto", cmap="cividis", origin="lower") +axes[1].set(xlabel="Past probe index", ylabel="Future response row") +axes[1].set_title("(b) Coupled response", loc="left", fontsize=11) +fig.colorbar(image, ax=axes[1], label=r"$|V_{\mu j}|$") +plt.show() ``` -## 5. Fit a Markovian noise digital twin (analytical optimization) +The weights $p_k=s_k^2/\sum_j s_j^2$ describe memory resolved by the sampled +probes. Without coupling, this example has one resolved mode; coupling reveals +additional modes. These weights are not the environment's state populations. See +{doc}`characterization` for probe choices and interpretation. -Learn Lindblad jump rates from observable trajectories with -{class}`~mqt.yaqs.NoiseCharacterizer` using **analytical optimization** -(simulator forward model + CMA-ES trajectory matching). +## Noise characterization -```{code-cell} ipython3 +Learn a dephasing rate from synthetic dynamics and compare the fitted +trajectories with the reference. + +```{code-cell} python import numpy as np from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseCharacterizer, NoiseModel, Observable, State -n_sites = 3 -sites = list(range(n_sites)) -hamiltonian = Hamiltonian.ising(n_sites, J=1.0, g=2.0) -init_state = State(n_sites, initial="zeros") -fitting_observables = [Observable("y", 0), Observable("z", 0), Observable("y", 1)] -sim_params = AnalogSimParams( - observables=fitting_observables, - elapsed_time=0.8, - dt=0.1, - order=1, - sample_timesteps=True, -) -reference_model = NoiseModel( - [{"name": "pauli_x", "sites": [s], "strength": 0.08} for s in sites] - + [{"name": "pauli_y", "sites": [s], "strength": 0.08} for s in sites] - + [{"name": "pauli_z", "sites": [s], "strength": 0.08} for s in sites] -) -init_guess = NoiseModel( - [{"name": "pauli_x", "sites": [s], "strength": 0.35} for s in sites] - + [{"name": "pauli_y", "sites": [s], "strength": 0.35} for s in sites] - + [{"name": "pauli_z", "sites": [s], "strength": 0.35} for s in sites] -) +hamiltonian = Hamiltonian.ising(2, J=1.0, g=1.0) +observables = [Observable("z", 0), Observable("y", 0)] +params = AnalogSimParams(observables=observables, elapsed_time=3.0, dt=0.1, preset="fast") +reference = NoiseModel([{"name": "pauli_z", "sites": [0], "strength": 0.18}]) +guess = NoiseModel([{"name": "pauli_z", "sites": [0], "strength": 0.6}]) -result = NoiseCharacterizer(show_progress=False).characterize( +characterizer = NoiseCharacterizer(show_progress=False) +fit = characterizer.characterize( hamiltonian, - sim_params, - init_state=init_state, - init_guess=init_guess, - observables=fitting_observables, - reference_model=reference_model, - x_low=np.zeros(len(init_guess.processes)), - x_up=np.full(len(init_guess.processes), 0.5), - sigma0=0.05, - popsize=8, + params, + init_state=State(2, initial="zeros"), + init_guess=guess, + observables=observables, + reference_model=reference, + x_low=np.array([0.0]), + x_up=np.array([1.0]), max_iter=20, - seed=42, ) +print("Fitted rate:", fit.best_parameters) +``` -times = result.times -obs_labels = [r"$\langle Y_0\rangle$", r"$\langle Z_0\rangle$", r"$\langle Y_1\rangle$"] -fig, axes = plt.subplots(1, 2, figsize=(8, 2.8), layout="constrained", sharey=True) -for ax, traj, title in zip(axes, [result.fit_traj, result.ref_traj], ["learned twin", "reference"], strict=True): - im = ax.imshow( - traj, - aspect="auto", - extent=(times[0], times[-1], len(obs_labels), 0), - vmin=-1, - vmax=1, - cmap="RdBu_r", - ) - ax.set_yticks([i + 0.5 for i in range(len(obs_labels))], obs_labels) - ax.set_xlabel("time") - ax.set_title(title) -fig.colorbar(im, ax=axes, shrink=0.9, label="expectation") -fig.suptitle(rf"Twin fit: RMSE={result.trajectory_rmse():.2e}", y=1.02) +```{code-cell} python +:tags: [hide-input] +fig, axes = plt.subplots(1, 2, figsize=(6.8, 3.0), width_ratios=[1.5, 1]) +for index, (color, label) in enumerate((("#0072B2", r"$\langle Z_0\rangle$"), ("#D55E00", r"$\langle Y_0\rangle$"))): + axes[0].plot(fit.times, fit.fit_traj[index], color=color, label=label) + axes[0].plot(fit.times[::2], fit.ref_traj[index, ::2], "o", color=color, markerfacecolor="white") +axes[0].plot([], [], "o", color="0.3", markerfacecolor="white", label="Reference") +axes[0].set(xlabel=r"Time $t$", ylabel="Expectation value", ylim=(-1.05, 1.05)) +axes[0].set_title("(a) Fitted dynamics", loc="left", fontsize=11) +axes[0].legend() +axes[1].bar([0, 1], [0.6, fit.best_parameters[0]], color=["#D55E00", "#0072B2"], width=0.55) +axes[1].axhline(0.18, color="0.3", linestyle="--", linewidth=1.1, label="Reference") +axes[1].set(xticks=[0, 1], xticklabels=["Initial guess", "Fit"], ylabel=r"Dephasing rate $\gamma$", ylim=(0, 0.7)) +axes[1].set_title("(b) Recovered rate", loc="left", fontsize=11) +axes[1].legend() +plt.show() ``` -See {doc}`digital_twin` for the full analytical-optimization workflow, -experimental-data fitting, held-out prediction, and MCWF fitting. +For measured data, supply `ref_expectations` instead of `reference_model`. See +{doc}`digital_twin` for data preparation and validation of the fitted model. -## 6. Train a surrogate and predict under controls +## Surrogate prediction -Train a causal surrogate with -{class}`~mqt.yaqs.memory_characterizer.MemoryCharacterizer`, then predict the -probe-qubit state after one or more control legs. Pass an explicit per-leg list -to compare different sequences on the same trained model. Surrogate training -requires PyTorch (`uv pip install mqt.yaqs[torch]`). +Train a model on control sequences, then compare its predictions on +**new sequences** with Hamiltonian calculations. Install the `torch` extra +first: `uv pip install "mqt.yaqs[torch]"`. -```{code-cell} ipython3 -rho0 = np.eye(2, dtype=np.complex128) / 2.0 -ham_sure = Hamiltonian.ising(length=2, J=1.0, g=1.0) +```{code-cell} python +import torch -model = mc.train( - ham_sure, - params, - num_interventions=1, - n=32, - train_kwargs={"epochs": 30, "batch_size": 8}, - model_kwargs={"d_model": 32, "nhead": 4, "num_layers": 1, "dim_ff": 64}, +from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer + +torch.manual_seed(7) +hamiltonian = Hamiltonian.ising(3, J=1.0, g=0.7) +params = AnalogSimParams(elapsed_time=0.2, dt=0.2, preset="fast") +characterizer = MemoryCharacterizer(show_progress=False) +model = characterizer.train( + hamiltonian, params, num_interventions=2, n=160, seed=7, + intervention_style="measure_prepare", ) +held_out = characterizer.sample( + hamiltonian, params, num_interventions=2, n=48, seed=99, + intervention_style="measure_prepare", +) +features, rho0, target = held_out.tensors +prediction = model.predict(features.numpy(), rho0.numpy(), return_numpy=True) +``` -hadamard = np.array([[1, 1], [1, -1]], dtype=np.complex128) / np.sqrt(2) -pauli_x = np.array([[0, 1], [1, 0]], dtype=np.complex128) -control_sequences = { - r"$\mathrm{H}$": [{"unitary": hadamard}], - r"$\mathrm{X}$": [{"unitary": pauli_x}], -} - -pauli_ops = { - "X": np.array([[0, 1], [1, 0]], dtype=np.complex128), - "Y": np.array([[0, -1j], [1j, 0]], dtype=np.complex128), - "Z": np.array([[1, 0], [0, -1]], dtype=np.complex128), -} -expectations = { - label: [ - float(np.trace(op @ mc.predict(model, rho0, controls, num_interventions=1)).real) - for op in pauli_ops.values() - ] - for label, controls in control_sequences.items() -} - -pauli_names = list(pauli_ops) -x = np.arange(len(pauli_names)) -width = 0.35 - -fig, ax = plt.subplots(figsize=(5, 3.5)) -for offset, (label, values) in zip((-width / 2, width / 2), expectations.items()): - ax.bar(x + offset, values, width, label=f"control {label}") -ax.set_xticks(x, pauli_names) -ax.set_ylabel("expectation value") -ax.set_title("Probe Pauli expectations for two control sequences") -ax.legend(frameon=False) -fig.tight_layout() +```{code-cell} python +:tags: [hide-input] +# Packed entries 0 and 6 are the two diagonal populations. +z_reference = target.numpy()[:, -1, 0] - target.numpy()[:, -1, 6] +z_prediction = prediction[:, -1, 0] - prediction[:, -1, 6] +rmse = np.sqrt(np.mean((z_prediction - z_reference)**2)) +fig, ax = plt.subplots(figsize=(3.8, 3.5)) +ax.plot([-1, 1], [-1, 1], "--", color="0.3", linewidth=1.0, label="Exact agreement") +ax.scatter(z_reference, z_prediction, s=25, color="#0072B2", alpha=0.8, edgecolor="white", linewidth=0.4) +ax.text(0.06, 0.94, f"RMSE = {rmse:.3f}", transform=ax.transAxes, va="top") +ax.set(xlabel=r"Hamiltonian reference $\langle Z\rangle$", ylabel=r"Surrogate prediction $\langle Z\rangle$", + xlim=(-1.05, 1.05), ylim=(-1.05, 1.05), xticks=[-1, 0, 1], yticks=[-1, 0, 1]) +ax.set_aspect("equal") +plt.show() ``` -`predict` also accepts a style string (for example `"haar"`) or a per-leg list -mixing unitaries and measure–prepare slots. See {doc}`characterization` for -environmental memory probing and {doc}`memory_surrogate` for held-out accuracy -checks and exact-reference validation. - -## 7. Where to go next - -| Goal | Start here | -| ---------------------------------------------------- | ----------------------------- | -| Environmental memory probing | {doc}`characterization` | -| Markovian noise digital-twin fitting | {doc}`digital_twin` | -| Surrogate training, prediction, and exact validation | {doc}`memory_surrogate` | -| Open-system dynamics, noise, time grids | {doc}`analog_simulation` | -| Bell-curve (log-normal) noise strengths | {doc}`realistic_noise_models` | -| Circuit observables, mid-circuit sampling, OpenQASM | {doc}`circuit_observables` | -| Accuracy presets and truncation knobs | {doc}`simulation_parameters` | -| Check two circuits for equivalence | {doc}`equivalence_checking` | - -## Related topics - -- {doc}`state_initialization` — `State` presets and representations -- {doc}`simulator_initialization` — parallelism, progress bars, `Result` fields -- {doc}`representation_comparison` — when to use MPS, MCWF, or Lindblad backends -- {doc}`equivalence_checking` — MPO backend, transpiler regression tests, - OpenQASM +The scatter tests observable prediction on 48 held-out sequences. It does not +certify every predicted density matrix or other control settings. See +{doc}`memory_surrogate` for broader accuracy checks and prediction through +{meth}`~mqt.yaqs.MemoryCharacterizer.predict`. + +## Next steps + +| Task | Guide | +| -------------------------------------- | -------------------------------- | +| Choose accuracy settings | {doc}`simulation_parameters` | +| Choose states and simulation backends | {doc}`state_initialization` | +| Combine analog evolution and circuits | {doc}`digital_analog_simulation` | +| Check whether two circuits agree | {doc}`equivalence_checking` | +| Learn noise models from dynamics | {doc}`digital_twin` | +| Study memory in a system's environment | {doc}`characterization` | +| Train models for dynamics with memory | {doc}`memory_surrogate` | From 23f5cbbd6c0b4012ee30b4d760b4e37d92032a47 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:23:35 +0200 Subject: [PATCH 04/30] updated quickstart --- docs/examples/quickstart.md | 424 ++++++++++++++++++++++++------------ 1 file changed, 281 insertions(+), 143 deletions(-) diff --git a/docs/examples/quickstart.md b/docs/examples/quickstart.md index 62e8637e8..82ccfb359 100644 --- a/docs/examples/quickstart.md +++ b/docs/examples/quickstart.md @@ -50,80 +50,92 @@ plt.rcParams.update({ }) ``` -## Large-scale analog dynamics +## Noisy analog dynamics -Follow one excitation as it spreads through a **50-site XY spin chain**. YAQS -uses an MPS rather than storing all $2^{50}$ state amplitudes. +Compare coherent transport and damping in a **20-site XY spin chain**. YAQS uses +an MPS and averages noisy dynamics over Monte Carlo trajectories. ```{code-cell} python -from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State +from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseModel, Observable, Simulator, State -length = 50 +length = 20 center = length // 2 basis = "0" * center + "1" + "0" * (length - center - 1) state = State(length, initial="basis", basis_string=basis) hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) params = AnalogSimParams( observables=[Observable("z", site) for site in range(length)], - elapsed_time=6.0, - dt=0.2, + elapsed_time=3.0, + dt=0.25, + num_traj=32, preset="fast", + random_seed=7, ) +relaxation_rate = 1.0 +noise = NoiseModel([ + {"name": "lowering", "sites": [site], "strength": relaxation_rate} for site in range(length) +]) simulator = Simulator(show_progress=False) -analog = simulator.run(state, hamiltonian, params) +coherent = simulator.run(state, hamiltonian, params) +dissipative = simulator.run(state, hamiltonian, params, noise) ``` ```{code-cell} python :tags: [hide-input] from matplotlib.colors import PowerNorm -occupation = (1 - np.asarray(analog.expectation_values).real) / 2 -sites = np.arange(length) -fig, axes = plt.subplots(1, 2, figsize=(7.0, 3.0), width_ratios=[1.2, 1]) -image = axes[0].pcolormesh( - analog.times, sites, occupation, shading="auto", cmap="cividis", - norm=PowerNorm(0.5, vmin=0, vmax=1), rasterized=True, -) -fig.colorbar(image, ax=axes[0], label=r"$\langle n_i\rangle$", ticks=[0, 0.1, 0.5, 1]) -axes[0].set(xlabel=r"Time $t$", ylabel=r"Site $i$", xlim=(0, 6), ylim=(-0.5, length - 0.5)) -axes[0].set_title("(a) Excitation transport", loc="left", fontsize=11) -for time, color, marker in zip((2, 4, 6), ("#0072B2", "#D55E00", "#009E73"), ("o", "s", "^"), strict=True): - index = np.argmin(np.abs(analog.times - time)) - axes[1].plot(sites, occupation[:, index], color=color, marker=marker, markevery=3, label=rf"$t={time}$") -axes[1].set(xlabel=r"Site $i$", ylabel=r"Occupation $\langle n_i\rangle$", xlim=(0, length - 1), ylim=(0, 0.22)) -axes[1].set_title("(b) Spatial profiles", loc="left", fontsize=11) -axes[1].legend() +occupation = np.stack([ + (1 - np.asarray(result.expectation_values).real) / 2 + for result in (coherent, dissipative) +]) +fig, axes = plt.subplots(1, 3, figsize=(7.2, 2.8)) +for ax, values, title in zip( + axes[:2], occupation, ("(a) Noiseless", "(b) Damped"), strict=True, +): + image = ax.pcolormesh( + coherent.times, np.arange(length), values, shading="auto", cmap="cividis", + norm=PowerNorm(0.5, vmin=0, vmax=1), rasterized=True, + ) + ax.set(xlabel=r"Time $t$", ylabel=r"Site $i$") + ax.set_title(title, loc="left", fontsize=11) +fig.colorbar(image, ax=list(axes[:2]), label=r"$\langle n_i\rangle$", ticks=[0, 0.1, 0.5, 1]) +axes[2].plot(coherent.times, occupation[0].sum(axis=0), color="#0072B2", label="Noiseless") +axes[2].plot(coherent.times, occupation[1].sum(axis=0), "o-", color="#D55E00", label="Damped") +axes[2].plot(coherent.times, np.exp(-relaxation_rate * coherent.times), "--", color="0.3", label="Damping law") +axes[2].set(xlabel=r"Time $t$", ylabel=r"Total excitation $\sum_i\langle n_i\rangle$", ylim=(0, 1.08)) +axes[2].set_title("(c) Excitation loss", loc="left", fontsize=11) +axes[2].legend() plt.show() ``` -The occupation $n_i=(1-Z_i)/2$ shows propagation and interference. The color -scale emphasizes small occupations; the total excitation remains one. This -low-excitation example stays inexpensive because its entanglement is limited. -For noise, larger trajectory budgets, and convergence checks, see -{doc}`analog_simulation` and {doc}`simulation_parameters`. +The occupation $n_i=(1-Z_i)/2$ shows propagation and interference. Both heatmaps +use the same color scale, which emphasizes small occupations. Damping removes +excitations; the 32-trajectory estimate fluctuates around the exponential loss +law. This low-excitation state has limited entanglement. See +{doc}`analog_simulation` and {doc}`simulation_parameters` for larger budgets and +convergence checks. ## Noisy circuit readout -Prepare an eight-qubit GHZ circuit and compare ideal and damped readout with 256 -shots per run. +Prepare a **16-qubit graph state** and compare noiseless and damped readout. ```{code-cell} python from qiskit import QuantumCircuit from mqt.yaqs import DigitalSimParams, NoiseModel, Simulator, State -num_qubits = 8 +num_qubits = 16 circuit = QuantumCircuit(num_qubits) -circuit.h(0) +circuit.h(range(num_qubits)) for site in range(num_qubits - 1): - circuit.cx(site, site + 1) + circuit.cz(site, site + 1) circuit.measure_all() state = State(num_qubits, initial="zeros") params = DigitalSimParams(shots=256, preset="fast", random_seed=7) noise = NoiseModel([ - {"name": "lowering", "sites": [site], "strength": 0.3} for site in range(num_qubits) + {"name": "lowering", "sites": [site], "strength": 0.5} for site in range(num_qubits) ]) simulator = Simulator(show_progress=False) @@ -134,36 +146,38 @@ damped = simulator.run(state, circuit, params, noise) ```{code-cell} python :tags: [hide-input] excitation_number = np.arange(num_qubits + 1) +readout_probabilities = [] fig, ax = plt.subplots(figsize=(5.4, 2.9)) -for result, offset, color, label in ( - (ideal, -0.18, "#0072B2", "Ideal"), - (damped, 0.18, "#D55E00", "Damped"), +for result, color, offset, label in ( + (ideal, "#0072B2", -0.22, "Noiseless"), + (damped, "#D55E00", 0.22, "Damped"), ): probability = np.zeros(num_qubits + 1) for outcome, count in result.counts.items(): probability[outcome.bit_count()] += count / params.shots - ax.bar(excitation_number + offset, probability, width=0.36, color=color, - edgecolor="white", linewidth=0.6, label=label) -ax.set(xlabel="Number of excited qubits", ylabel="Measured probability", xticks=excitation_number, ylim=(0, 0.7)) + readout_probabilities.append(probability) + ax.bar(excitation_number + offset, probability, width=0.42, color=color, + edgecolor="white", linewidth=0.5, alpha=0.9, label=label) +ax.set(xlabel="Number of excited qubits", ylabel="Measured probability", + xlim=(-0.5, num_qubits + 0.5), xticks=np.arange(0, num_qubits + 1, 2)) ax.legend() plt.show() ``` -The ideal populations lie at zero and eight excitations. Damping shifts weight -toward lower excitation numbers. This histogram summarizes readout populations; -see {doc}`circuit_shots` for bitstring counts and {doc}`circuit_observables` for -expectation values and OpenQASM input. +Damping shifts the readout toward fewer excitations. Each histogram summarizes +256 shots. See {doc}`circuit_shots` for bitstring counts and +{doc}`circuit_observables` for expectation values and OpenQASM input. ## Circuit equivalence -Verify a transpiled circuit, then measure how an extra $Z$ rotation changes its -agreement with the original. +Verify a transpiled circuit, then compare the effects of an added rotation and +increasing Pauli noise. ```{code-cell} python import numpy as np from qiskit import QuantumCircuit, transpile -from mqt.yaqs import EquivalenceChecker +from mqt.yaqs import EquivalenceChecker, NoiseModel circuit = QuantumCircuit(4) circuit.h(0) @@ -174,43 +188,62 @@ decomposed = transpile(circuit, basis_gates=["rz", "sx", "x", "cx"]) checker = EquivalenceChecker() print("Equivalent:", checker.check(circuit, decomposed)["equivalent"]) angles = np.linspace(0, np.pi, 17) -overlaps = [] +rotation_overlaps = [] for angle in angles: perturbed = decomposed.copy() perturbed.rz(float(angle), 0) - overlaps.append(checker.check(circuit, perturbed)["fidelity"]) + rotation_overlaps.append(checker.check(circuit, perturbed)["fidelity"]) + +error_probabilities = np.linspace(0, 0.9, 10) +noisy_checks = [] +for probability in error_probabilities: + noise = NoiseModel([{"name": "pauli_z", "sites": [0], "strength": float(probability)}]) + noisy_checks.append(checker.check( + circuit, decomposed, noise_model=noise, num_traj=128, random_seed=7, + )) ``` ```{code-cell} python :tags: [hide-input] -fig, ax = plt.subplots(figsize=(4.6, 2.9)) -ax.plot(angles, overlaps, "o", color="#0072B2", label="YAQS") -ax.plot(angles, np.abs(np.cos(angles / 2)), "--", color="0.3", label=r"Analytic $|\cos(\theta/2)|$") -ax.set(xlabel=r"Added rotation $\theta$ (rad)", ylabel="Normalized operator overlap", - xlim=(0, np.pi), ylim=(0, 1.05), xticks=[0, np.pi / 2, np.pi], - xticklabels=["0", r"$\pi/2$", r"$\pi$"]) -ax.legend() +fig, axes = plt.subplots(1, 2, figsize=(6.8, 2.9), sharey=True) +axes[0].plot(angles, rotation_overlaps, "o", color="#0072B2", label="YAQS") +axes[0].plot(angles, np.abs(np.cos(angles / 2)), "--", color="0.3", label=r"$|\cos(\theta/2)|$") +axes[0].set(xlabel=r"Added rotation $\theta$ (rad)", ylabel="Normalized overlap", + xlim=(-0.03, np.pi + 0.03), ylim=(0, 1.05), xticks=[0, np.pi / 2, np.pi], + xticklabels=["0", r"$\pi/2$", r"$\pi$"]) +axes[0].set_title("(a) Coherent error", loc="left", fontsize=11) +axes[0].legend() +axes[1].errorbar(error_probabilities, [check["fidelity"] for check in noisy_checks], + yerr=[check["fidelity_error"] for check in noisy_checks], + fmt="o", color="#D55E00", capsize=2, label="YAQS") +axes[1].plot(error_probabilities, np.sqrt(1 - error_probabilities), "--", color="0.3", label=r"$\sqrt{1-p}$") +axes[1].set(xlabel=r"Pauli error probability $p$", xlim=(-0.03, 0.93)) +axes[1].set_title("(b) Stochastic error", loc="left", fontsize=11) +axes[1].legend() plt.show() ``` -An overlap of one indicates agreement up to a global phase. The added rotation -produces a controlled difference with a known analytic overlap. See -{doc}`equivalence_checking` for noise and accuracy controls. +An overlap of one indicates agreement up to a global phase. Here, site 0 has one +noise opportunity, after the first CX gate. A $Z$ error has zero overlap, so the +ensemble's root-mean-square overlap is $\sqrt{1-p}$. Error bars show Monte Carlo +standard errors. Noise is applied to the second circuit. See +{doc}`equivalence_checking` for supported noise and accuracy controls. ## Environmental memory -Compare the response modes of a qubit with and without coupling to a two-spin -environment, using the same probe grid. +Sweep the Ising coupling in a three-spin chain and compare the probe qubit's +memory spectra using the same probe grid. ```{code-cell} python import numpy as np from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer +couplings = np.linspace(0, 1.5, 13) params = AnalogSimParams(elapsed_time=0.5, dt=0.5, preset="fast") characterizer = MemoryCharacterizer(show_progress=False) memories = [] -for coupling in (0.0, 1.0): +for coupling in couplings: hamiltonian = Hamiltonian.ising(3, J=coupling, g=1.0) memories.append(characterizer.characterize( hamiltonian, params, num_interventions=4, cut=2, preset="quick", @@ -220,127 +253,232 @@ for coupling in (0.0, 1.0): ```{code-cell} python :tags: [hide-input] -fig, axes = plt.subplots(1, 2, figsize=(6.8, 3.0)) -for memory, color, marker, label in zip( - memories, ("#D55E00", "#0072B2"), ("s", "o"), ("Uncoupled", "Coupled"), strict=True, -): +fig, axes = plt.subplots(1, 2, figsize=(6.8, 3.0), width_ratios=[1.4, 1]) +colors = plt.colormaps["Reds"](np.linspace(0.35, 0.95, len(couplings))) +for memory, color in zip(memories, colors, strict=True): spectrum = memory.singular_values(2) weights = spectrum**2 / np.sum(spectrum**2) - axes[0].semilogy(np.arange(1, len(weights) + 1), weights, marker=marker, color=color, label=label) -axes[0].set(xlabel="Mode index", ylabel=r"Resolved mode weight $p_k$", ylim=(1e-10, 2)) -axes[0].set_title("(a) Memory spectrum", loc="left", fontsize=11) -axes[0].legend() -image = axes[1].imshow(np.abs(memories[1].response_matrix(2)), aspect="auto", cmap="cividis", origin="lower") -axes[1].set(xlabel="Past probe index", ylabel="Future response row") -axes[1].set_title("(b) Coupled response", loc="left", fontsize=11) -fig.colorbar(image, ax=axes[1], label=r"$|V_{\mu j}|$") + axes[0].semilogy(np.arange(1, len(weights) + 1), weights, "o-", + color=color, linewidth=1.2, markersize=3) +axes[0].text(0.28, 0.14, "Coupling increases", transform=axes[0].transAxes, + ha="center", va="center", fontsize=10, color="black") +axes[0].annotate("", xy=(0.82, 0.76), xytext=(0.50, 0.20), + xycoords="axes fraction", + arrowprops={"arrowstyle": "->", "color": "black", + "lw": 1.5, "connectionstyle": "arc3,rad=0.25"}) +axes[0].set(xlabel="Mode index", ylabel=r"Resolved mode weight $p_k$", ylim=(1e-13, 2)) +axes[0].set_title("(a) Memory spectra", loc="left", fontsize=11) +entropies = np.array([memory.entropy(2) for memory in memories]) +axes[1].fill_between(couplings, entropies, color=colors[3], alpha=0.3) +axes[1].plot(couplings, entropies, "o-", color=colors[-1], linewidth=2.6, + markerfacecolor="white", markeredgewidth=1.2) +axes[1].set(xlabel=r"Coupling $J$", ylabel=r"Memory entropy $S_V$", + xlim=(-0.03, 1.53), ylim=(-0.008, 0.34), xticks=[0, 0.5, 1, 1.5]) +axes[1].grid(axis="y", color="0.9", linewidth=0.5) +axes[1].set_axisbelow(True) +axes[1].set_title("(b) Resolved memory", loc="left", fontsize=11) plt.show() ``` The weights $p_k=s_k^2/\sum_j s_j^2$ describe memory resolved by the sampled -probes. Without coupling, this example has one resolved mode; coupling reveals -additional modes. These weights are not the environment's state populations. See -{doc}`characterization` for probe choices and interpretation. +probes. Darker curves show larger $J$. Coupling redistributes weight among the +resolved modes. The entropy measures this spread and peaks within this sweep. +These weights are not environment-state populations. See {doc}`characterization` +for probe choices and interpretation. -## Noise characterization +## Create a digital twin -Learn a dephasing rate from synthetic dynamics and compare the fitted -trajectories with the reference. +Learn +**relaxation and dephasing from measurements at the ends of a spin chain**. Then +rerun the fitted model to predict transport through the unmeasured interior. ```{code-cell} python import numpy as np -from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseCharacterizer, NoiseModel, Observable, State +from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseCharacterizer, NoiseModel, Observable, Simulator, State -hamiltonian = Hamiltonian.ising(2, J=1.0, g=1.0) -observables = [Observable("z", 0), Observable("y", 0)] -params = AnalogSimParams(observables=observables, elapsed_time=3.0, dt=0.1, preset="fast") -reference = NoiseModel([{"name": "pauli_z", "sites": [0], "strength": 0.18}]) -guess = NoiseModel([{"name": "pauli_z", "sites": [0], "strength": 0.6}]) +length = 4 +state = State(length, initial="basis", basis_string="1000", representation="density_matrix") +hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) +observables = [Observable("z", site) for site in range(length)] +params = AnalogSimParams(observables=observables, elapsed_time=8.0, dt=0.1, preset="fast") +reference = NoiseModel([ + {"name": "lowering", "sites": [3], "strength": 0.35}, + {"name": "pauli_z", "sites": [2], "strength": 0.12}, +]) +guess = NoiseModel([ + {"name": "lowering", "sites": [3], "strength": 0.2}, + {"name": "pauli_z", "sites": [2], "strength": 0.2}, +]) +simulator = Simulator(show_progress=False) +measured = simulator.run(state, hamiltonian, params, reference) characterizer = NoiseCharacterizer(show_progress=False) fit = characterizer.characterize( hamiltonian, params, - init_state=State(2, initial="zeros"), + init_state=state, init_guess=guess, - observables=observables, - reference_model=reference, - x_low=np.array([0.0]), - x_up=np.array([1.0]), - max_iter=20, + observables=[observables[0], observables[-1]], + ref_expectations=np.asarray(measured.expectation_values)[[0, -1]], + x_low=np.zeros(2), + x_up=np.ones(2), + max_iter=40, + seed=7, ) -print("Fitted rate:", fit.best_parameters) +reconstructed = simulator.run(state, hamiltonian, params, fit.optimal_model) +print("Fitted rates:", fit.best_parameters.round(3)) ``` ```{code-cell} python :tags: [hide-input] -fig, axes = plt.subplots(1, 2, figsize=(6.8, 3.0), width_ratios=[1.5, 1]) -for index, (color, label) in enumerate((("#0072B2", r"$\langle Z_0\rangle$"), ("#D55E00", r"$\langle Y_0\rangle$"))): - axes[0].plot(fit.times, fit.fit_traj[index], color=color, label=label) - axes[0].plot(fit.times[::2], fit.ref_traj[index, ::2], "o", color=color, markerfacecolor="white") -axes[0].plot([], [], "o", color="0.3", markerfacecolor="white", label="Reference") -axes[0].set(xlabel=r"Time $t$", ylabel="Expectation value", ylim=(-1.05, 1.05)) -axes[0].set_title("(a) Fitted dynamics", loc="left", fontsize=11) -axes[0].legend() -axes[1].bar([0, 1], [0.6, fit.best_parameters[0]], color=["#D55E00", "#0072B2"], width=0.55) -axes[1].axhline(0.18, color="0.3", linestyle="--", linewidth=1.1, label="Reference") -axes[1].set(xticks=[0, 1], xticklabels=["Initial guess", "Fit"], ylabel=r"Dephasing rate $\gamma$", ylim=(0, 0.7)) -axes[1].set_title("(b) Recovered rate", loc="left", fontsize=11) -axes[1].legend() +reference_dynamics = (1 - np.asarray(measured.expectation_values).real) / 2 +fitted_dynamics = (1 - np.asarray(reconstructed.expectation_values).real) / 2 +fig, axes = plt.subplots(1, 2, figsize=(6.8, 2.8), sharey=True) +for ax, dynamics, title in zip( + axes, (reference_dynamics, fitted_dynamics), + ("(a) Reference transport", "(b) Fitted model"), strict=True, +): + image = ax.pcolormesh(measured.times, np.arange(length), dynamics, + shading="auto", cmap="cividis", vmin=0, vmax=1, rasterized=True) + ax.set(xlabel=r"Time $t$", yticks=np.arange(length)) + ax.set_title(title, loc="left", fontsize=11) +axes[0].set_ylabel(r"Site $i$") +fig.colorbar(image, ax=list(axes), label=r"$\langle n_i\rangle$", ticks=[0, 0.5, 1]) plt.show() ``` -For measured data, supply `ref_expectations` instead of `reference_model`. See -{doc}`digital_twin` for data preparation and validation of the fitted model. - -## Surrogate prediction - -Train a model on control sequences, then compare its predictions on -**new sequences** with Hamiltonian calculations. Install the `torch` extra -first: `uv pip install "mqt.yaqs[torch]"`. +The excitation propagates and reflects while a local sink removes population and +dephasing changes transport. Only sites 0 and 3 enter the fit; sites 1 and 2 +check its predictions. Both heatmaps share a scale. This example uses synthetic +observations without measurement noise from density-matrix simulation; replace +the reference array with your measured traces. The fit assumes the two channel +types and their sites are known. See {doc}`digital_twin` for data preparation +and validation. + +## Predict non-Markovian dynamics + +Train a surrogate on **random unitary controls**, then explore how a pulse +changes the coherence of a probe coupled to an environment spin. Install the +`torch` extra first: `uv pip install "mqt.yaqs[torch]"`. + +```{note} +**Experimental feature.** Surrogate modeling is not yet supported by a published +YAQS paper. This example uses a short, two-intervention horizon. Validate +predictions for your chosen controls and time horizon against reference +simulations or measurements. +``` ```{code-cell} python +import numpy as np import torch from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer -torch.manual_seed(7) -hamiltonian = Hamiltonian.ising(3, J=1.0, g=0.7) -params = AnalogSimParams(elapsed_time=0.2, dt=0.2, preset="fast") +num_steps = 2 +interval = 0.6 +hamiltonian = Hamiltonian.ising(2, J=1.0, g=0.5) +params = AnalogSimParams(elapsed_time=interval, dt=interval, preset="fast") characterizer = MemoryCharacterizer(show_progress=False) -model = characterizer.train( - hamiltonian, params, num_interventions=2, n=160, seed=7, - intervention_style="measure_prepare", +``` + +Train on 4,096 random sequences and select the model using 256 fresh random +validation sequences. The environment starts in $|0\rangle$. The folded cell +sets a small model and training budget for this documentation example. + +```{code-cell} python +:tags: [hide-input] +torch.manual_seed(7) +schedule = [0.0] + [interval] * num_steps +validation = characterizer.sample( + hamiltonian, params, num_interventions=num_steps, n=256, seed=99, + timesteps=schedule, intervention_style="haar", ) -held_out = characterizer.sample( - hamiltonian, params, num_interventions=2, n=48, seed=99, - intervention_style="measure_prepare", +model = characterizer.train( + hamiltonian, params, num_interventions=num_steps, n=4096, seed=7, + timesteps=schedule, intervention_style="haar", + model_kwargs={"d_model": 64, "num_layers": 2, "dim_ff": 128}, + train_kwargs={"epochs": 400, "lr": 1e-3, "val_dataset": validation}, ) -features, rho0, target = held_out.tensors -prediction = model.predict(features.numpy(), rho0.numpy(), return_numpy=True) +``` + +Start the probe in $|+\rangle$. Let it evolve for $t=0.6$, apply a rotation +$R_z(\theta)$, then predict its state at $t=1.2$ for each pulse angle. These +chosen sequences are not supplied during training. + +```{code-cell} python +plus = np.array([1, 1], dtype=complex) / np.sqrt(2) +rho0 = np.outer(plus, plus.conj()) +identity = np.eye(2, dtype=complex) +pulse_angles = np.linspace(0, 2 * np.pi, 61) +predicted_states = np.asarray([ + characterizer.predict(model, rho0, [ + {"unitary": identity}, + {"unitary": np.diag(np.exp(-0.5j * angle * np.array([1, -1])))}, + ]) + for angle in pulse_angles +]) ``` ```{code-cell} python :tags: [hide-input] -# Packed entries 0 and 6 are the two diagonal populations. -z_reference = target.numpy()[:, -1, 0] - target.numpy()[:, -1, 6] -z_prediction = prediction[:, -1, 0] - prediction[:, -1, 6] -rmse = np.sqrt(np.mean((z_prediction - z_reference)**2)) -fig, ax = plt.subplots(figsize=(3.8, 3.5)) -ax.plot([-1, 1], [-1, 1], "--", color="0.3", linewidth=1.0, label="Exact agreement") -ax.scatter(z_reference, z_prediction, s=25, color="#0072B2", alpha=0.8, edgecolor="white", linewidth=0.4) -ax.text(0.06, 0.94, f"RMSE = {rmse:.3f}", transform=ax.transAxes, va="top") -ax.set(xlabel=r"Hamiltonian reference $\langle Z\rangle$", ylabel=r"Surrogate prediction $\langle Z\rangle$", - xlim=(-1.05, 1.05), ylim=(-1.05, 1.05), xticks=[-1, 0, 1], yticks=[-1, 0, 1]) -ax.set_aspect("equal") +from matplotlib.collections import LineCollection +from matplotlib.patches import Circle + +predicted_coherence = 2 * np.abs(predicted_states[:, 0, 1]) +no_pulse_coherence = predicted_coherence[0] +fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.4), gridspec_kw={"width_ratios": [1, 1.4]}) + +# Show the final states projected onto the equatorial Bloch plane. +bloch_xy = np.column_stack(( + 2 * predicted_states[:, 0, 1].real, + -2 * predicted_states[:, 0, 1].imag, +)) +points = bloch_xy[:, None, :] +segments = np.concatenate((points[:-1], points[1:]), axis=1) +trajectory = LineCollection(segments, cmap="twilight_shifted", + norm=plt.Normalize(0, 2 * np.pi), linewidth=2.6) +trajectory.set_array((pulse_angles[:-1] + pulse_angles[1:]) / 2) +axes[0].add_patch(Circle((0, 0), 1, facecolor="0.97", edgecolor="0.75", linewidth=0.8)) +axes[0].add_patch(Circle((0, 0), 0.5, fill=False, edgecolor="0.85", linewidth=0.6)) +axes[0].axhline(0, color="0.85", linewidth=0.6) +axes[0].axvline(0, color="0.85", linewidth=0.6) +axes[0].add_collection(trajectory) +axes[0].plot(*bloch_xy[0], "o", color="0.3", markerfacecolor="white", markersize=6) +axes[0].set(xlabel=r"$\langle X\rangle$", ylabel=r"$\langle Y\rangle$", + xlim=(-1.05, 1.05), ylim=(-1.05, 1.05), aspect="equal", + xticks=[-1, 0, 1], yticks=[-1, 0, 1]) +axes[0].set_title("(a) Final probe state", loc="left", fontsize=11) +colorbar = fig.colorbar(trajectory, ax=axes[0], orientation="horizontal", + shrink=0.8, pad=0.08, aspect=25, ticks=[0, np.pi, 2 * np.pi]) +colorbar.ax.set_xticklabels(["0", r"$\pi$", r"$2\pi$"]) +colorbar.set_label(r"Pulse angle $\theta$") + +axes[1].fill_between(pulse_angles, no_pulse_coherence, predicted_coherence, + where=predicted_coherence >= no_pulse_coherence, + interpolate=True, color="#0072B2", alpha=0.15) +axes[1].fill_between(pulse_angles, no_pulse_coherence, predicted_coherence, + where=predicted_coherence < no_pulse_coherence, + interpolate=True, color="#D55E00", alpha=0.2) +axes[1].plot(pulse_angles, predicted_coherence, color="#0072B2", linewidth=2.2, + label="With control pulse") +axes[1].axhline(no_pulse_coherence, color="0.4", linestyle="--", linewidth=1.1, + label="Free evolution") +axes[1].set(xlabel=r"Pulse angle $\theta$", ylabel=r"Final coherence $2|\rho_{01}|$", + xlim=(0, 2 * np.pi), ylim=(0, 1), + xticks=[0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi], + xticklabels=["0", r"$\pi/2$", r"$\pi$", r"$3\pi/2$", r"$2\pi$"]) +axes[1].set_title("(b) Predicted coherence", loc="left", fontsize=11) +axes[1].legend(loc="upper right", fontsize=9) plt.show() ``` -The scatter tests observable prediction on 48 held-out sequences. It does not -certify every predicted density matrix or other control settings. See -{doc}`memory_surrogate` for broader accuracy checks and prediction through -{meth}`~mqt.yaqs.MemoryCharacterizer.predict`. +The left panel shows the predicted final probe states in the Bloch plane; color +identifies the pulse angle, and the open circle marks free evolution. The right +panel shows their coherence. Blue shading marks an increase over free evolution; +orange marks a decrease. All points use the same trained model and include both +evolution intervals. See {doc}`memory_surrogate` for validation and checks of +predicted density matrices. ## Next steps @@ -350,6 +488,6 @@ certify every predicted density matrix or other control settings. See | Choose states and simulation backends | {doc}`state_initialization` | | Combine analog evolution and circuits | {doc}`digital_analog_simulation` | | Check whether two circuits agree | {doc}`equivalence_checking` | -| Learn noise models from dynamics | {doc}`digital_twin` | +| Create a digital twin | {doc}`digital_twin` | | Study memory in a system's environment | {doc}`characterization` | -| Train models for dynamics with memory | {doc}`memory_surrogate` | +| Predict non-Markovian dynamics | {doc}`memory_surrogate` | From ca75b2eecce6d2c8c4df796fd23d000fd4a8b16e Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:04:13 +0200 Subject: [PATCH 05/30] updated indexing --- docs/index.md | 93 +++++++++++++++++++-------------------------------- 1 file changed, 35 insertions(+), 58 deletions(-) diff --git a/docs/index.md b/docs/index.md index 76220d385..64449156c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -77,104 +77,81 @@ flowchart LR | Custom gate translation | {doc}`examples/custom_gates` | ```{toctree} -:caption: Getting started +:caption: Start here :hidden: :maxdepth: 1 :titlesonly: -installation -examples/quickstart -examples/state_initialization -examples/simulator_initialization -examples/simulation_parameters +Installation +Quickstart ``` ```{toctree} -:caption: Analog simulation +:caption: Simulation setup :hidden: :maxdepth: 1 :titlesonly: -examples/hamiltonians -examples/analog_simulation -examples/realistic_noise_models -examples/scheduled_jumps -examples/ensemble_evolution -examples/representation_comparison -examples/transmon_emulation -examples/trapped_ion +Quantum states +Hamiltonians +Noise models +State representations +Simulation parameters +Simulator configuration ``` ```{toctree} -:caption: Environmental Memory Characterization +:caption: Simulation workflows :hidden: :maxdepth: 1 :titlesonly: -examples/characterization -examples/memory_surrogate +Analog simulation +Circuit observables +Circuit shots +Analog-digital simulation ``` ```{toctree} -:caption: Digital Twin +:caption: Characterization and verification :hidden: :maxdepth: 1 :titlesonly: -examples/digital_twin +Environmental memory +Noise characterization +Circuit equivalence ``` ```{toctree} -:caption: Digital Circuit Simulation +:caption: Advanced examples :hidden: :maxdepth: 1 :titlesonly: -examples/circuit_observables -examples/circuit_shots -examples/custom_gates -examples/equivalence_checking -``` - -```{toctree} -:caption: Digital–analog simulation -:hidden: -:maxdepth: 1 -:titlesonly: +Ensemble evolution +Scheduled jumps +Custom gates +Transmon emulation +Trapped-ion emulation +Surrogate models -examples/digital_analog_simulation ``` ```{toctree} -:caption: Reference +:caption: Reference and contributing :hidden: :maxdepth: 1 :titlesonly: -references -CHANGELOG -UPGRADING -``` - -```{toctree} -:caption: Developers -:hidden: -:maxdepth: 1 -:titlesonly: - -contributing -ai_usage -tooling -support -``` - -```{toctree} -:caption: API Reference -:hidden: -:glob: -:maxdepth: 1 - -api/mqt/yaqs/index +API reference +References and citations +Changelog +Upgrade guide +Contributing +AI usage +Development tools +Support ``` ## Contributors and Supporters From 7e642bad4e0e0910ab8191a81ec6b8cc47096ac4 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:07:45 +0200 Subject: [PATCH 06/30] label update --- docs/index.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/index.md b/docs/index.md index 64449156c..cef4bcba3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# MQT YAQS — Scalable simulation and characterization for open systems, noisy circuits, and realistic hardware +# MQT YAQS — Simulation and characterization of quantum systems and their environments MQT YAQS (pronounced "yaks" like the animals) is a Python library designed for **scalable, computationally efficient** simulation and characterization of open @@ -50,7 +50,7 @@ flowchart LR sim --> result ``` -### Learning paths +### Find a guide | I want to… | Read | | -------------------------------------------------------------------------- | --------------------------------------------------------------------------- | @@ -107,8 +107,8 @@ Simulator configuration :titlesonly: Analog simulation -Circuit observables -Circuit shots +Circuit simulation +Circuit measurements Analog-digital simulation ``` @@ -129,12 +129,12 @@ Circuit equivalence :maxdepth: 1 :titlesonly: -Ensemble evolution +Ensembles and correlations Scheduled jumps Custom gates Transmon emulation Trapped-ion emulation -Surrogate models +Surrogate models (experimental) ``` @@ -154,7 +154,7 @@ Development tools Support ``` -## Contributors and Supporters +## Contributors and supporters The _[Munich Quantum Toolkit (MQT)](https://mqt.readthedocs.io)_ is developed by the [Chair for Design Automation](https://www.cda.cit.tum.de/) at the From c05c10e84f1d43f3dafc9ff5e0640b1ed53ad080 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:44:00 +0200 Subject: [PATCH 07/30] updated analog simulation --- docs/examples/analog_simulation.md | 381 ++++++++++++++++++++--------- 1 file changed, 264 insertions(+), 117 deletions(-) diff --git a/docs/examples/analog_simulation.md b/docs/examples/analog_simulation.md index a85435211..f29d2b47f 100644 --- a/docs/examples/analog_simulation.md +++ b/docs/examples/analog_simulation.md @@ -2,176 +2,323 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true - execution_timeout: 600 + execution_timeout: 180 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Noisy Analog Simulation -This guide walks through an open-system **analog** simulation with the tensor -jump method (TJM): build a Hamiltonian, attach a noise model, configure -{class}`~mqt.yaqs.core.data_structures.simulation_parameters.AnalogSimParams`, -and visualize time-resolved observables. +Relaxation can remove an excitation before it travels across a spin chain. To +study this competition between transport and loss, we follow an excitation +through a **20-site XY chain** and compare its motion at several relaxation +rates. The occupation heatmaps show where the excitation travels, while the +total occupation tells us how much survives. + +This guide extends the analog example in {doc}`quickstart` using only the +standard YAQS installation. YAQS represents the state as a matrix product state +(MPS) and simulates noise with the tensor jump method (TJM), averaging +independent quantum-jump trajectories. Run the cells in order in a notebook. In +a script, put execution inside an `if __name__ == "__main__":` guard, as shown +in {doc}`simulator_initialization`. + +## 1. Build the Hamiltonian -For log-normal disorder on strengths and static calibration spread, see -{doc}`realistic_noise_models`. For execution options (parallelism, progress -bars), see {doc}`simulator_initialization`. To build Ising, Hubbard, -Pauli-string, or hardware Hamiltonians, see {doc}`hamiltonians`. +The XY model lets an excitation move between neighboring spins without changing +the total number of excitations. With open ends and the coefficients below, its +Hamiltonian is -## 1. Hamiltonian +$$ +H=-\frac{1}{2}\sum_{i=0}^{L-2}(X_iX_{i+1}+Y_iY_{i+1}). +$$ -```{code-cell} ipython3 +```{code-cell} python from mqt.yaqs import Hamiltonian -L = 5 -J, g = 1.0, 0.8 -H_0 = Hamiltonian.ising(L, J, g) +length = 20 +hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) ``` -See {doc}`hamiltonians` for Pauli sums, Fermi–Hubbard, Bose–Hubbard, and -coupled-transmon factories. +Setting `Jz=0` removes the ZZ interaction from the Heisenberg model. The default +boundary condition is open, so sites 0 and 19 have one neighbor each. Because +the Hamiltonian conserves excitation, a noiseless run gives us a baseline +against which to measure relaxation. -## 2. Initial state and noise model +We use $\hbar=1$ and set the excitation-hopping amplitude to one. Time is +measured in its inverse units. See {doc}`hamiltonians` for other built-in +models, custom terms, and boundary conditions. -We prepare a Néel state $\ket{01010\ldots}$ and track staggered magnetization -under a transverse-field Ising model with on-site amplitude damping. The -alternating $\langle Z_i \rangle$ pattern at $t=0$ spreads and decays in a -site-dependent way. +## 2. Prepare one localized excitation -```{code-cell} ipython3 -from mqt.yaqs import NoiseModel, State +To see the excitation spread, start every spin in $|0\rangle$ except the spin at +site 10, which starts in $|1\rangle$. Each character of `basis_string` specifies +the state of the corresponding site, starting at site 0. -state = State(L, initial="Neel") +```{code-cell} python +from mqt.yaqs import State -gamma = 0.08 -noise_model = NoiseModel([ - {"name": "lowering", "sites": [i], "strength": gamma} for i in range(L) -]) +center = length // 2 +basis = "0" * center + "1" + "0" * (length - center - 1) +state = State(length, initial="basis", basis_string=basis) ``` -Pass a float for each `strength` here. For distribution-valued strengths -(log-normal and other distributions), see {doc}`realistic_noise_models`. +`State` uses an MPS by default. A single excitation keeps entanglement modest, +so this example is inexpensive compared with a general interacting state on 20 +sites. For other initial states and representations, see +{doc}`state_initialization` and {doc}`representation_comparison`. + +## 3. Choose observables, times, and accuracy -## 3. Simulation parameters +The spatial dynamics require one observable per site. We use $Z_i$, whose +expectation gives the occupation through +$\langle n_i\rangle=(1-\langle Z_i\rangle)/2$. -```{code-cell} ipython3 +```{code-cell} python from mqt.yaqs import AnalogSimParams, Observable -sim_params = AnalogSimParams( - observables=[Observable("z", site) for site in range(L)], - elapsed_time=6.0, - dt=0.1, - num_traj=20, - max_bond_dim=16, - svd_threshold=1e-6, - order=2, - sample_timesteps=True, +params = AnalogSimParams( + observables=[Observable("z", site) for site in range(length)], + elapsed_time=3.0, + dt=0.25, + num_traj=32, + preset="fast", + random_seed=7, ) ``` -Optional `tdvp_sweeps` (default `1`) runs multiple symmetric TDVP substeps per -physical step `dt`, improving unitary accuracy without changing the noise -timestep. +These settings sample 13 times from $t=0$ to $t=3$, long enough to see the +excitation spread away from the center. Time sampling is enabled by default, and +`elapsed_time` must be an integer multiple of `dt`. -**Evolution integrator:** analog simulations default to `EvolutionMode.TDVP` -(two-site TDVP sweeps). Switch to BUG with: +The `fast` preset sets numerical tolerances for a quick example. We override its +trajectory budget with `num_traj=32`, so each noisy run averages 32 +trajectories; the noiseless calculation needs only one. Increasing this budget +reduces sampling error, while timestep and MPS truncation errors require +separate convergence checks. -```{code-cell} ipython3 -from mqt.yaqs import AnalogSimParams, EvolutionMode +`random_seed` fixes the jump random streams for repeat runs with the same +configuration. It does not improve accuracy. See {doc}`simulation_parameters` +for presets, overrides, and convergence settings. -bug_params = AnalogSimParams( - evolution_mode=EvolutionMode.BUG, - elapsed_time=0.1, - dt=0.1, -) -``` +## 4. Define relaxation + +Relaxation competes with the motion generated by the Hamiltonian. The `lowering` +channel turns $|1\rangle$ into $|0\rangle$ without adding new excitations. Give +every site the same relaxation rate $\gamma$: -That uses center-augmented alternating-endpoint BUG composition with one -compression and renormalization after each ``dt`` step. +```{code-cell} python +from mqt.yaqs import NoiseModel -## 4. Reproducible stochastic runs +relaxation_rate = 4.0 +noise = NoiseModel([ + {"name": "lowering", "sites": [site], "strength": relaxation_rate} + for site in range(length) +]) +``` -With `num_traj > 1`, each {meth}`~mqt.yaqs.Simulator.run` call averages -independent quantum-jump trajectories. Set -{attr}`~mqt.yaqs.core.data_structures.simulation_parameters.AnalogSimParams.random_seed` -to fix the pseudorandom stream across trajectories (and for distribution-valued -noise strengths): +For analog evolution, `strength` is a Lindblad rate, with units of inverse time. +YAQS forms the jump operator $\sqrt{\gamma}\,|0\rangle\langle1|$ internally; +supply $\gamma$, not its square root. -```{code-cell} ipython3 -import copy +The corresponding lifetime is $1/\gamma$. At $\gamma=4$, loss occurs on a +shorter timescale than hopping between sites. For site-dependent rates, other +channels, custom operators, and distribution-valued strengths, see +{doc}`realistic_noise_models`. -import numpy as np +## 5. Run the noiseless and noisy cases -from mqt.yaqs import AnalogSimParams, Observable, Simulator - -repro_params = AnalogSimParams( - observables=[Observable("z", site) for site in range(L)], - elapsed_time=1.0, - dt=0.1, - num_traj=16, - max_bond_dim=4, - svd_threshold=1e-6, - order=2, - sample_timesteps=True, - random_seed=42, -) +Initialize the simulator separately, then pass the state, Hamiltonian, and +parameters to `run`. Omit the noise model for the noiseless baseline. -sim = Simulator(parallel=True, show_progress=False) +```{code-cell} python +from mqt.yaqs import Simulator +simulator = Simulator(show_progress=False) +noiseless = simulator.run(state, hamiltonian, params) +noisy = simulator.run(state, hamiltonian, params, noise) +``` -def run_reproducible() -> list[np.ndarray]: - st = copy.deepcopy(state) - params = copy.deepcopy(repro_params) - result = sim.run(st, H_0, params, copy.deepcopy(noise_model)) - return result.expectation_values +Parallel execution remains enabled by default. `show_progress=False` keeps the +documentation quiet; omit it to see progress. Execution and worker options are +explained in {doc}`simulator_initialization`. +## 6. Read the results -first_run = run_reproducible() -second_run = run_reproducible() -``` +`expectation_values` follows the order of the supplied observables. Here, row +$i$ contains $\langle Z_i\rangle$ and columns follow `times`. Convert the +expectations to an array of occupations: -Circuit simulations expose the same `random_seed` setting through -{class}`~mqt.yaqs.DigitalSimParams`. +```{code-cell} python +import numpy as np -## 5. Run and visualize +times = np.asarray(noisy.times) +occupation = (1 - np.asarray(noisy.expectation_values)) / 2 +print(occupation.shape) +``` -```{code-cell} ipython3 -result = sim.run(state, H_0, sim_params, noise_model) +The shape is `(20, 13)`: sites by sampled times. At $t=0$, only site 10 has +occupation one. In noisy runs, each entry is a trajectory average, rather than a +single measurement outcome. `noisy.trajectories` retains the per-trajectory +observable data when you need to examine sampling fluctuations. + +## 7. Compare four relaxation strengths + +The relaxation lifetime determines how long the excitation can propagate before +loss. To compare that timescale with the transport dynamics, keep the model and +numerical settings fixed and add rates between the two runs above: + +```{code-cell} python +rates = [0.0, 0.5, 1.5, relaxation_rate] +results = {0.0: noiseless, relaxation_rate: noisy} +for rate in rates[1:-1]: + rate_noise = NoiseModel([ + {"name": "lowering", "sites": [site], "strength": rate} + for site in range(length) + ]) + results[rate] = simulator.run(state, hamiltonian, params, rate_noise) + +occupations = np.stack([ + (1 - np.asarray(results[rate].expectation_values)) / 2 + for rate in rates +]) ``` -```{code-cell} ipython3 ---- -mystnb: - image: - width: 80% - align: center ---- -import matplotlib.pyplot as plt +The nonzero rates give lifetimes of $2$, $2/3$, and $1/4$ in the time units used +here. The heatmaps below compare how far the excitation spreads before its +occupation decays. All panels use one square-root color scale to keep small +occupations visible, with no normalization by the remaining excitation. -heatmap = result.expectation_values +```{code-cell} python +:tags: [hide-input] +import matplotlib.pyplot as plt +from matplotlib.colors import PowerNorm +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", + "font.serif": ["STIXGeneral"], + "mathtext.fontset": "stix", + "font.size": 11, + "axes.labelsize": 11, + "axes.linewidth": 0.7, + "xtick.labelsize": 10, + "ytick.labelsize": 10, + "xtick.direction": "in", + "ytick.direction": "in", + "xtick.top": True, + "ytick.right": True, + "legend.fontsize": 9, + "legend.frameon": False, + "lines.linewidth": 1.6, + "figure.constrained_layout.use": True, + "savefig.dpi": 180, +}) +fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.4), sharex=True, sharey=True) +for ax, values, rate, panel in zip( + axes.flat, occupations, rates, "abcd", strict=True, +): + image = ax.pcolormesh( + times, np.arange(length), values, shading="auto", cmap="cividis", + norm=PowerNorm(0.5, vmin=0, vmax=1), rasterized=True, + ) + label = "Noiseless" if rate == 0 else rf"$\gamma={rate:g}$" + ax.set_title(f"({panel}) {label}", loc="left", fontsize=11) + ax.set(xticks=[0, 1, 2, 3], yticks=[0, 5, 10, 15, 19]) +for ax in axes[-1]: + ax.set_xlabel(r"Time $t$") +for ax in axes[:, 0]: + ax.set_ylabel(r"Site $i$") +fig.colorbar(image, ax=axes.ravel().tolist(), + label=r"Occupation $\langle n_i\rangle$", ticks=[0, 0.1, 0.5, 1]) +plt.show() +``` -fig, ax = plt.subplots(figsize=(7, 4), layout="constrained") -im = ax.imshow(heatmap, aspect="auto", extent=(0, 6, L, 0), vmin=-1, vmax=1) -ax.set_xlabel("Time") -ax.set_yticks([x - 0.5 for x in range(1, L + 1)], [str(x) for x in range(L)]) -ax.set_ylabel("Site") -fig.colorbar(im, ax=ax, shrink=0.9, label=r"$\langle Z \rangle$") +**Excitation transport under uniform relaxation.** Without noise, the excitation +spreads outward and forms an interference pattern. Increasing the relaxation +rate suppresses occupation at later times and more distant sites. At $\gamma=4$, +most of the excitation is lost before it travels far from the center. + +The fading pattern does not imply slower propagation. Conditioned on no jump, +the excitation follows the noiseless spatial dynamics because this Hamiltonian +conserves excitation and the loss rate is uniform. Site-dependent relaxation or +a different channel can change that profile. This time window mainly shows +outward propagation; longer runs can also show reflections from the open ends. + +## 8. Check the total excitation + +The heatmaps combine spreading with loss. Summing occupation over sites removes +the spatial information and isolates the survival probability. Since the initial +state has exactly one excitation and every site has the same relaxation rate, +the ensemble mean obeys + +$$ +N(t)=\sum_i\langle n_i(t)\rangle=e^{-\gamma t}. +$$ + +```{code-cell} python +:tags: [hide-input] +total_excitation = occupations.sum(axis=1) +fine_times = np.linspace(times[0], times[-1], 200) +colors = ["0.2", "#0072B2", "#009E73", "#D55E00"] +fig, ax = plt.subplots(figsize=(6.2, 3.2)) +for rate, values, color in zip(rates, total_excitation, colors, strict=True): + ax.plot(times, values, "o-", color=color, markersize=4, + label="Noiseless" if rate == 0 else rf"$\gamma={rate:g}$") + ax.plot(fine_times, np.exp(-rate * fine_times), "--", color=color, + linewidth=1.1, alpha=0.75) +ax.set(xlabel=r"Time $t$", ylabel=r"Total excitation $N(t)$", + xlim=(0, 3), ylim=(0, 1.08), xticks=[0, 1, 2, 3]) +ax.legend(ncols=4, loc="lower center", bbox_to_anchor=(0.5, 1.02)) plt.show() ``` +**Excitation survival compared with the decay law.** Markers show the YAQS +estimates, and dashed lines show $e^{-\gamma t}$. At $t=3$, the exact survival +probabilities are one without noise and about $0.22$, $0.011$, and +$6\times10^{-6}$ for the three nonzero rates. + +Each noisy trajectory either retains its excitation or loses it to a jump. With +only 32 trajectories, the estimated survival fraction changes in steps and may +reach zero while the exact mean is still positive. Finite sampling can also make +curves for different rates cross. Increase `num_traj` to reduce these +fluctuations; the sampling error decreases as $1/\sqrt{\mathtt{num\_traj}}$. + +Together, the two figures distinguish loss from redistribution along the chain. +Uniform relaxation reduces the chance of finding the excitation, while surviving +excitations retain the coherent transport pattern. This conclusion and the decay +law rely on uniform loss and excitation-conserving dynamics. The noiseless total +also checks conservation, but that check alone cannot establish the accuracy of +the spatial dynamics. + +## Accuracy and other options + +Before using a larger or more demanding model, check convergence separately: + +| Change | What to check | +| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Increase `num_traj` | Whether noisy observables stabilize within sampling uncertainty. | +| Reduce `dt` at fixed duration | Whether the dynamics stabilize with a finer evolution step. | +| Use `balanced` or `accurate` | Whether tighter truncation and solver settings change the observables. Keep an explicit trajectory budget when comparing numerical settings. | +| Increase `length` or `elapsed_time` | Whether boundaries, entanglement growth, and runtime affect the question being studied. | + +The default MPS evolution uses TDVP updates. To use the BUG integrator, import +`EvolutionMode` from `mqt.yaqs` and set `evolution_mode=EvolutionMode.BUG` in +`AnalogSimParams`. For TDVP, `tdvp_sweeps` adds unitary substeps without +changing the noise timestep. See {doc}`simulation_parameters` for these advanced +choices. + ## Related topics -- {doc}`digital_analog_simulation` — combine analog evolution with digital - operations in one program -- {doc}`hamiltonians` — Pauli, Hubbard, hardware, and piecewise time-dependent +- {doc}`realistic_noise_models` — other channels, custom jumps, and static + disorder +- {doc}`digital_analog_simulation` — combine analog evolution and digital + operations +- {doc}`hamiltonians` — built-in, custom, hardware, and time-dependent Hamiltonians -- {doc}`representation_comparison` — MPS, MCWF, and Lindblad backends +- {doc}`representation_comparison` — MPS, statevector, and density matrix + backends - {doc}`scheduled_jumps` — deterministic jumps at specified times - {doc}`ensemble_evolution` — unitary ensemble correlations -- {doc}`quickstart` — minimal first simulation From 9270ed075a15e5a85374fc47719762f4abfc4b06 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:57:05 +0200 Subject: [PATCH 08/30] updated shot-based circuit sim --- docs/examples/circuit_shots.md | 291 ++++++++++++++++++++++----------- 1 file changed, 193 insertions(+), 98 deletions(-) diff --git a/docs/examples/circuit_shots.md b/docs/examples/circuit_shots.md index f4c728748..e471f9a89 100644 --- a/docs/examples/circuit_shots.md +++ b/docs/examples/circuit_shots.md @@ -2,141 +2,236 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true - execution_timeout: 600 + execution_timeout: 180 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Shot-Based Circuit Simulation -Digital circuit simulation can sample computational-basis **shots** after -circuit evolution, mimicking hardware readout statistics. Use -{class}`~mqt.yaqs.DigitalSimParams` with `shots=...` and read bitstring counts -from {attr}`~mqt.yaqs.Result.counts`. +Relaxation changes which bitstrings a quantum circuit produces. We study this +change by preparing a **16-qubit graph state** and sampling its readout at +several damping strengths. Grouping the outcomes by the number of excited qubits +makes the loss of excitation visible without plotting all $2^{16}$ bitstrings. -For expectation-value simulation and mid-circuit observables, see -{doc}`circuit_observables`. For parameter presets and truncation settings, see -{doc}`simulation_parameters`. +This guide extends the circuit-readout example in {doc}`quickstart` using the +standard YAQS installation. YAQS evolves a matrix product state (MPS) through +the circuit and samples the final state in the computational basis. Run the +cells in order in a notebook. For a script, use the `if __name__ == "__main__":` +guard shown in {doc}`simulator_initialization`. -You can pass an OpenQASM file path or raw OpenQASM string to -{meth}`~mqt.yaqs.Simulator.run` instead of building a -{class}`qiskit.circuit.QuantumCircuit` in Python (OpenQASM 3 requires -`uv pip install mqt-yaqs[qasm3]`). +## 1. Prepare the circuit and initial state -## 1. Circuit +Start from $|0\rangle^{\otimes16}$, apply a Hadamard gate to each qubit, then +entangle neighboring qubits with CZ gates. These gates change relative phases +without changing computational-basis probabilities, so the ideal readout has +equal probability for every bitstring. -We use a shallow randomized ansatz—single-qubit $R_y$ rotations followed by a -linear chain of $CZ$ gates—typical of variational benchmarks. +```{code-cell} python +from qiskit import QuantumCircuit -```{code-cell} ipython3 -import numpy as np -from qiskit.circuit import QuantumCircuit +from mqt.yaqs import State -num_qubits = 6 +num_qubits = 16 circuit = QuantumCircuit(num_qubits) -rng = np.random.default_rng(42) -for i in range(num_qubits): - circuit.ry(float(rng.uniform(0.6, 2.2)), i) -for i in range(num_qubits - 1): - circuit.cz(i, i + 1) +circuit.h(range(num_qubits)) +for site in range(num_qubits - 1): + circuit.cz(site, site + 1) circuit.measure_all() + +state = State(num_qubits, initial="zeros") ``` -## 2. Initial state and noise model +`State` uses an MPS by default. `shots` requests final computational-basis +sampling, including when a circuit has no explicit measurement gates. YAQS +accepts the terminal measurements above; it does not support measurements +followed by further gates on the measured qubits or classical feedback. -```{code-cell} ipython3 -from mqt.yaqs import NoiseModel, State +## 2. Set the sampling budget and damping -state = State(num_qubits, initial="zeros") +Each shot produces one bitstring. We use 256 shots per run to keep the example +quick, with the `fast` preset controlling numerical tolerances. -gamma = 0.5 -noise_model = NoiseModel([ - {"name": "lowering", "sites": [i], "strength": gamma} for i in range(num_qubits) +```{code-cell} python +from mqt.yaqs import DigitalSimParams, NoiseModel + +params = DigitalSimParams(shots=256, preset="fast", random_seed=7) +damping_rate = 1.5 +noise = NoiseModel([ + {"name": "lowering", "sites": [site], "strength": damping_rate} + for site in range(num_qubits) ]) ``` -Amplitude damping relaxes each qubit toward $\ket{0}$. During circuit execution -the noise channels compete with unitary spreading, so readout mass shifts toward -the all-zeros bitstring compared with the noiseless run. +The `lowering` channel relaxes $|1\rangle$ toward $|0\rangle$. In circuit +simulation, `strength` is a Lindblad rate per unit of gate noise time, rather +than a direct error probability. YAQS applies one unit of noise time after each +gate on two or more qubits, using only noise processes supported entirely on +that gate's qubits. Single-qubit gates and idle sites receive no noise. + +Here, each end qubit participates in one CZ gate, while each interior qubit +participates in two. The noise therefore acts during entangling operations, +rather than as a separate readout-error channel. Simulate a transpiled circuit +when its compiled gates should determine the noise opportunities. -## 3. Simulation parameters and run +`shots` sets the total sample budget and must be supplied explicitly. In a noisy +shots-only run, YAQS uses one stochastic trajectory per shot. The noiseless run +evolves once and samples that final state repeatedly. Setting `num_traj` does +not change a shots-only budget. See {doc}`simulation_parameters` for presets and +the separate roles of shots and trajectories. -`DigitalSimParams` requires an explicit `shots` count (not covered by accuracy -presets). We run the **same** circuit twice: once without noise (ideal readout -statistics) and once with on-site amplitude damping. +## 3. Run the noiseless and noisy cases -```{code-cell} ipython3 -from mqt.yaqs import Simulator, DigitalSimParams +Use the same circuit, state, and parameters for both runs so that the comparison +isolates the added noise. Initialize the simulator separately, then omit the +noise model for the baseline. -sim_params = DigitalSimParams(shots=1024, max_bond_dim=16, svd_threshold=1e-6, random_seed=7) +```{code-cell} python +from mqt.yaqs import Simulator -sim = Simulator(show_progress=False) -result_clean = sim.run(state, circuit, sim_params) -result_noisy = sim.run(state, circuit, sim_params, noise_model) +simulator = Simulator(show_progress=False) +ideal = simulator.run(state, circuit, params) +damped = simulator.run(state, circuit, params, noise) ``` -For log-normal disorder on relaxation rates, see {doc}`realistic_noise_models`. -To collect observables and shots in one call, set both fields on -{class}`~mqt.yaqs.DigitalSimParams`. In a noisy combined run, `shots` is the -**total** sample budget distributed across `num_traj` stochastic trajectories; -see {doc}`simulation_parameters`. +Parallel execution remains enabled by default. `show_progress=False` suppresses +bars in the documentation; omit it to see progress. The seed makes the sampling +repeatable for the same configuration, but does not reduce sampling error. -## 4. Noiseless vs noisy readout histogram +## 4. Read individual outcomes -Bitstrings are sorted lexicographically among **low Hamming-weight** outcomes -(at most two excitations), where amplitude damping concentrates probability. -`Result.counts` keys are integers (site 0 is the least-significant bit); see -{doc}`simulation_parameters` for the encoding. +`Result.counts` maps integer outcomes to their counts. Site 0 is the +least-significant bit, so outcome 1 means that only site 0 is excited. +Formatting an outcome as binary places site 0 on the right. -```{code-cell} ipython3 ---- -mystnb: - image: - width: 90% - align: center ---- -import matplotlib.pyplot as plt +```{code-cell} python +most_common = sorted(damped.counts.items(), key=lambda item: item[1], reverse=True)[:5] +for outcome, count in most_common: + bitstring = format(outcome, f"0{num_qubits}b") + print(f"{bitstring}: {count} shots, estimated probability {count / params.shots:.3f}") +``` + +The counts sum to 256. An outcome absent from the dictionary was not observed; +its underlying probability need not be zero. To read a specific qubit, use +`(outcome >> site) & 1`. Bitstring counts preserve spatial information that the +grouped histogram below discards. + +## 5. Compare damping strengths + +The quickstart compares a noiseless run with one damped run. Here we add two +rates to show how the distribution moves as damping increases, reusing the +baseline and strong-noise calculations above. + +```{code-cell} python import numpy as np -def format_bitstring(key: int, num_bits: int) -> str: - """Format a little-endian integer outcome as a zero-padded bitstring.""" - return format(key, f"0{num_bits}b") - -def hamming_weight(key: int) -> int: - return key.bit_count() - -# Low-weight outcomes (|0...0> and nearby strings) where T1 noise accumulates -keys = sorted( - k - for k in set(result_clean.counts) | set(result_noisy.counts) - if hamming_weight(k) <= 2 -) -bitstrings = [format_bitstring(k, num_qubits) for k in keys] -x = np.arange(len(keys)) -width = 0.38 - -clean_vals = [result_clean.counts.get(k, 0) for k in keys] -noisy_vals = [result_noisy.counts.get(k, 0) for k in keys] - -fig, ax = plt.subplots(figsize=(9, 4), layout="constrained") -ax.bar(x - width / 2, clean_vals, width, label="noiseless", color="black", alpha=0.75) -ax.bar(x + width / 2, noisy_vals, width, label="noisy (amplitude damping)", color="tab:orange", alpha=0.85) -ax.set_xticks(x) -ax.set_xticklabels(bitstrings, rotation=45, ha="right", fontsize=8) -ax.set_xlabel("Bitstring (Hamming weight $\\leq 2$)") -ax.set_ylabel("Counts") -ax.set_title(f"Shot readout: relaxation drives counts toward $|0\\rangle^{{\\otimes {num_qubits}}}$") -ax.legend() -ax.grid(alpha=0.3, axis="y") +rates = [0.0, 0.1, 0.5, damping_rate] +results = {0.0: ideal, damping_rate: damped} +for rate in rates[1:-1]: + rate_noise = NoiseModel([ + {"name": "lowering", "sites": [site], "strength": rate} + for site in range(num_qubits) + ]) + results[rate] = simulator.run(state, circuit, params, rate_noise) + +probabilities = np.zeros((len(rates), num_qubits + 1)) +for row, rate in enumerate(rates): + for outcome, count in results[rate].counts.items(): + probabilities[row, outcome.bit_count()] += count / params.shots +``` + +`outcome.bit_count()` gives the number of excited qubits, often called the +Hamming weight. Each row of `probabilities` has 17 bins, from zero to 16 +excitations, and sums to one. Every outcome contributes, including the tails of +the distribution. + +The following histograms use shared axes. The gray outline repeats the sampled +noiseless baseline in the noisy panels so that the shift remains easy to +compare. + +```{code-cell} python +:tags: [hide-input] +import matplotlib.pyplot as plt +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", + "font.serif": ["STIXGeneral"], + "mathtext.fontset": "stix", + "font.size": 11, + "axes.labelsize": 11, + "axes.linewidth": 0.7, + "xtick.labelsize": 10, + "ytick.labelsize": 10, + "xtick.direction": "in", + "ytick.direction": "in", + "xtick.top": True, + "ytick.right": True, + "legend.fontsize": 9, + "legend.frameon": False, + "figure.constrained_layout.use": True, + "savefig.dpi": 180, +}) +excitation_number = np.arange(num_qubits + 1) +colors = ["#0072B2", "#009E73", "#D55E00", "#CC79A7"] +fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.6), sharex=True, sharey=True) +for ax, rate, probability, color, panel in zip( + axes.flat, rates, probabilities, colors, "abcd", strict=True, +): + if rate != 0: + ax.bar(excitation_number, probabilities[0], width=0.85, + facecolor="none", edgecolor="0.55", linewidth=0.9, label="Noiseless") + ax.bar(excitation_number, probability, width=0.65, color=color, + edgecolor="white", linewidth=0.4, alpha=0.9, + label="Noiseless" if rate == 0 else "Damped") + label = "Noiseless" if rate == 0 else rf"$\gamma={rate:g}$" + ax.set_title(f"({panel}) {label}", loc="left", fontsize=11) + ax.set(xlim=(-0.7, num_qubits + 0.7), xticks=np.arange(0, num_qubits + 1, 4)) + ax.legend(loc="upper right") +for ax in axes[-1]: + ax.set_xlabel("Number of excited qubits") +for ax in axes[:, 0]: + ax.set_ylabel("Measured probability") plt.show() ``` +**Readout shifts toward fewer excitations as damping increases.** Each panel +contains 256 shots from the same 16-qubit circuit. The noiseless distribution is +centered near eight excitations, while strong damping concentrates probability +near zero. The baseline outlines use the same samples in all panels. + +Without noise, each qubit has excitation probability $1/2$, so the excitation +number follows a binomial distribution. The graph state's phases do not appear +in this measurement basis. A matching histogram alone therefore cannot verify +that the intended entangled state was prepared. + +With noise, the shift measures excitation loss during the CZ gates. Different +bitstrings can have the same excitation number, so use `counts` or site-resolved +observables when the spatial distribution matters. For a bin of probability $p$, +independent shots have sampling uncertainty of order +$\sqrt{p(1-p)/\mathtt{shots}}$. Increase `shots` to reduce these fluctuations; +use tighter presets separately to check numerical error. + +## Other measurement workflows + +To obtain expectation values instead of counts, supply `observables` on +`DigitalSimParams`; see {doc}`circuit_observables`. You can request both outputs +in one call. In a noisy combined run, `num_traj` sets the observable ensemble, +and YAQS distributes the total `shots` across those trajectories. Multiple shots +from one trajectory share its noise history, so their uncertainty differs from +independent one-shot trajectories. + +You can pass an OpenQASM source string or file path to `Simulator.run` in place +of a Qiskit circuit. OpenQASM 3 requires the `qasm3` extra. See +{doc}`circuit_observables` for an executable example, mid-circuit observable +checkpoints, and gate-application modes. + ## Related topics -- {doc}`circuit_observables` — expectation values and mid-circuit sampling +- {doc}`simulation_parameters` — sampling budgets and accuracy presets +- {doc}`realistic_noise_models` — other channels, custom operators, and disorder - {doc}`custom_gates` — custom unitaries and gate translation +- {doc}`equivalence_checking` — compare circuit behavior From ea004f4c7c95ccf459297312693d979c1b9cc2f0 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:16:56 +0200 Subject: [PATCH 09/30] updated circuit simulation --- docs/examples/circuit_observables.md | 416 ++++++++++++++++----------- 1 file changed, 255 insertions(+), 161 deletions(-) diff --git a/docs/examples/circuit_observables.md b/docs/examples/circuit_observables.md index db1464745..d2be1517e 100644 --- a/docs/examples/circuit_observables.md +++ b/docs/examples/circuit_observables.md @@ -2,205 +2,299 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true - execution_timeout: 600 + execution_timeout: 300 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` +# Observable-Based Circuit Simulation -# Circuit Observables +The excitation transport in {doc}`analog_simulation` can also be simulated with +a quantum circuit. We use the same **20-site XY chain**, localized excitation, +and relaxation rates, then replace continuous Hamiltonian evolution with short +sequences of exchange gates. Sampling observables between these sequences lets +us reconstruct the occupation heatmaps and compare digital and analog dynamics. -Evolve a matrix-product state (MPS) through a Qiskit circuit and evaluate Pauli -(or custom) observables. Pass observables on {class}`~mqt.yaqs.DigitalSimParams` -and an optional {class}`~mqt.yaqs.NoiseModel` as the fourth argument to -{meth}`~mqt.yaqs.Simulator.run` for open-system tensor-jump trajectories; omit -it for a single unitary path (regardless of `num_traj`). +The example uses the standard YAQS installation and public interfaces. Run the +cells in order in a notebook; in a script, use the entry-point guard in +{doc}`simulator_initialization`. For bitstring counts rather than expectation +values, see {doc}`circuit_shots`. -For computational-basis shot histograms, see {doc}`circuit_shots`. You can also -request observables and `shots` together on one `DigitalSimParams`. +## 1. Turn the XY Hamiltonian into gates -| Workflow | Typical use | Key settings | -| --------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| **Final observables** | Noise scaling, benchmarking, device studies | `DigitalSimParams(observables=...)` evaluated after the last gate | -| **Mid-circuit observables** | Layer-wise diagnostics, depth-dependent calibration | `sample_layers=True` plus `barrier(label="SAMPLE_OBSERVABLES")` markers in the circuit | -| **Shot-based readout** | Hardware-like bitstring statistics | `DigitalSimParams(shots=...)` — see {doc}`circuit_shots` | +The Hamiltonian exchanges excitations between neighboring sites, -Circuits enter YAQS as {class}`qiskit.circuit.QuantumCircuit` objects (or -OpenQASM strings). The initial state should use `representation="mps"` (the -default for {class}`~mqt.yaqs.core.data_structures.state.State` presets). For -accuracy presets, truncation knobs, and `random_seed`, see -{doc}`simulation_parameters`. For log-normal disorder on noise strengths, see -{doc}`realistic_noise_models`. +$$ +H=-\frac{1}{2}\sum_{i=0}^{L-2}(X_iX_{i+1}+Y_iY_{i+1}). +$$ -```{code-cell} ipython3 -import matplotlib.pyplot as plt -import numpy as np +As in the analog guide, we set the hopping amplitude to one and use $\hbar=1$. +An exchange gate on sites $i$ and $i+1$ implements their contribution to the +evolution. Qiskit's `XXPlusYYGate(-2 * duration)` gives +$\exp[+i\,\mathtt{duration}(XX+YY)/2]$, with the sign set by this Hamiltonian. -from mqt.yaqs import Simulator +Gates on overlapping bonds do not commute. We approximate a time step with half +a step on even bonds, a full step on odd bonds, then another half step on even +bonds. This symmetric Trotter formula approaches the Hamiltonian dynamics as the +step decreases. Each exchange gate preserves excitation number, including when +noise acts between gates. -sim = Simulator(show_progress=False) +```{code-cell} python +import numpy as np +from qiskit import QuantumCircuit +from qiskit.circuit.library import XXPlusYYGate + + +def xy_circuit(length, dt, steps): + """Build a symmetric XY Trotter circuit and count gate exposures per step.""" + step_circuit = QuantumCircuit(length) + gate_exposures = np.zeros(length, dtype=int) + for parity, fraction in ((0, 0.5), (1, 1.0), (0, 0.5)): + for site in range(parity, length - 1, 2): + step_circuit.append(XXPlusYYGate(-2 * dt * fraction), [site, site + 1]) + gate_exposures[[site, site + 1]] += 1 + + circuit = QuantumCircuit(length) + for step in range(steps): + circuit.compose(step_circuit, inplace=True) + if step < steps - 1: + circuit.barrier(label="SAMPLE_OBSERVABLES") + return circuit, gate_exposures + + +length = 20 +dt = 0.25 +steps = 12 +circuit, gate_exposures = xy_circuit(length, dt, steps) ``` -## 1. Minimal run: unitary vs open-system noise +The circuit represents evolution to $t=3$. Labelled barriers separate the +Trotter steps so YAQS can sample observables at the same 13 times as the analog +guide. `gate_exposures` counts each site's two-qubit gates in one step; we will +use these counts to match the relaxation rate. -Evolve a short Trotterized Ising circuit and compare final $\langle Z_i\rangle$ -without noise and with on-site amplitude damping: +## 2. Prepare the state and observables -```{code-cell} ipython3 -from mqt.yaqs import NoiseModel, Observable, State, DigitalSimParams -from mqt.yaqs.core.libraries.circuit_library import create_ising_circuit - -num_qubits = 3 -qc = create_ising_circuit(L=num_qubits, J=1.0, g=0.8, dt=0.1, timesteps=6) -circuit_state = State(num_qubits, initial="zeros") -circuit_params = DigitalSimParams( - observables=[Observable("z", site) for site in range(num_qubits)], +Start with one excitation at site 10. Measuring $Z_i$ at every site gives the +occupation through $\langle n_i\rangle=(1-\langle Z_i\rangle)/2$. + +```{code-cell} python +from mqt.yaqs import DigitalSimParams, NoiseModel, Observable, Simulator, State + +center = length // 2 +basis = "0" * center + "1" + "0" * (length - center - 1) +state = State(length, initial="basis", basis_string=basis) +observables = [Observable("z", site) for site in range(length)] +params = DigitalSimParams( + observables=observables, + sample_layers=True, + num_traj=16, preset="fast", - num_traj=32, + random_seed=7, ) -noise_model = NoiseModel([ - {"name": "lowering", "sites": [site], "strength": 0.05} for site in range(num_qubits) -]) +sim = Simulator(show_progress=False) +``` -clean_result = sim.run(circuit_state, qc, circuit_params) -noisy_result = sim.run(State(num_qubits, initial="zeros"), qc, circuit_params, noise_model) -clean_z = np.array([float(np.real(v[0])) for v in clean_result.expectation_values]) -noisy_z = np.array([float(np.real(v[0])) for v in noisy_result.expectation_values]) - -fig, ax = plt.subplots(figsize=(5, 3), layout="constrained") -x = np.arange(num_qubits) -bar_width = 0.35 -ax.bar(x - bar_width / 2, clean_z, bar_width, label="unitary", color="0.55") -ax.bar(x + bar_width / 2, noisy_z, bar_width, label="with damping", color="C0") -ax.set_xticks(x, [rf"$\langle Z_{i}\rangle$" for i in range(num_qubits)]) -ax.set_ylim(-1.05, 1.05) -ax.set_ylabel("expectation value") -ax.set_title("Optional noise model on the fourth `run` argument") -ax.legend(frameon=False) +`sample_layers=True` records observables at the circuit start, at each barrier +labelled `SAMPLE_OBSERVABLES`, and after the final gates. Barrier labels are +case-insensitive; unlabelled barriers do not trigger sampling. These checkpoints +evaluate expectations without collapsing the state, unlike hardware mid-circuit +measurements. + +The noisy results average 16 trajectories, while a noiseless run uses one. +Parallel execution remains enabled. The documentation suppresses progress bars; +omit `show_progress=False` to see them. The preset controls numerical +tolerances, while `num_traj` controls sampling uncertainty. + +## 3. Match relaxation to circuit steps + +The analog model uses a uniform relaxation rate $\gamma$ per unit of physical +time. Circuit noise instead acts for one unit of noise time after each gate on +two or more qubits, using only processes supported on that gate's qubits. +Single-qubit gates and idle sites receive no noise. + +Using the same numerical strength for every gate would make damping depend on +the number of gates rather than the represented duration. For a site involved in +$m_i$ gates per Trotter step, set its circuit strength to + +$$ +\mathtt{strength}_i=\frac{\gamma\,\Delta t}{m_i}. +$$ + +Here the end sites encounter two gates per step and interior sites encounter +three. Dividing by these counts gives each site a total relaxation exposure of +$\gamma\Delta t$ per step. Noise is still interleaved with the gates, so the +finite-step evolution can differ from the continuous analog model. This rate +mapping describes a digital approximation to that model; hardware gate noise +should instead follow the device and its compiled operations. + +```{code-cell} python +rates = [0.0, 0.5, 1.5, 4.0] +results = {} +for rate in rates: + noise = None if rate == 0 else NoiseModel([ + {"name": "lowering", "sites": [site], + "strength": rate * dt / gate_exposures[site]} + for site in range(length) + ]) + results[rate] = sim.run(state, circuit, params, noise) ``` -In digital simulation the noise model is applied once per gate on two or more -qubits: after the unitary update, one noise layer acts on processes whose sites -all belong to the gate's qubits. Single-qubit gates and idle sites between the -gate qubits receive no noise. Each gate counts as one unit of noise time; to -model compiled depth, simulate the transpiled circuit. +## 4. Read the sampled dynamics -## 2. Noise-strength sweep +`expectation_values` contains one NumPy array per observable. The observable +order follows the supplied list, and each array follows checkpoint order. +Combine the arrays and convert $Z_i$ to occupation: -On a longer chain, sweep a global relaxation rate $\gamma$ and track how each -qubit's final $\langle Z_i \rangle$ moves toward $+1$ as damping dominates: +```{code-cell} python +times = dt * np.arange(steps + 1) +occupations = np.stack([ + (1 - np.asarray(results[rate].expectation_values).real) / 2 + for rate in rates +]) +print(occupations.shape) +``` -```{code-cell} ipython3 -num_qubits = 5 -circuit = create_ising_circuit(L=num_qubits, J=1.0, g=0.5, dt=0.1, timesteps=10) -state = State(num_qubits, initial="zeros") -sim_params = DigitalSimParams( - observables=[Observable("z", site) for site in range(num_qubits)], - num_traj=64, - max_bond_dim=8, - svd_threshold=1e-6, -) +The shape is `(4, 20, 13)`: relaxation rates by sites by checkpoints. Digital +results can store complex values; `.real` selects the real expectation of these +Hermitian observables. `result.trajectories` retains the per-trajectory data. -gammas = [1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1] -heatmap = np.empty((num_qubits, len(gammas))) -for j, gamma in enumerate(gammas): - damping = NoiseModel([ - {"name": "lowering", "sites": [site], "strength": gamma} for site in range(num_qubits) - ]) - result = sim.run(state, circuit, sim_params, damping) - for i in range(num_qubits): - heatmap[i, j] = float(np.real(result.expectation_values[i][0])) - -fig, ax = plt.subplots(figsize=(7, 4), layout="constrained") -colors = plt.cm.viridis(np.linspace(0.15, 0.85, num_qubits)) -for i in range(num_qubits): - ax.semilogx(gammas, heatmap[i], "o-", color=colors[i], linewidth=1.8, markersize=5, label=rf"$q_{i}$") -ax.set_xlabel(r"Relaxation rate $\gamma$") -ax.set_ylabel(r"$\langle Z_i \rangle$") -ax.set_ylim(-1.05, 1.05) -ax.legend(ncol=num_qubits, fontsize=8, loc="lower left", frameon=False) -ax.set_title("Final magnetization vs damping strength") -ax.grid(alpha=0.3, which="both") -``` +A standalone circuit has `result.times=None` because its gates do not define +physical durations. The `times` array above assigns physical times from our +Trotter construction. For final observables only, omit `sample_layers=True`; +each observable then has one sample. To collect counts too, supply `shots` and +YAQS distributes that total budget across the noisy observable trajectories. -## 3. Mid-circuit observables +## 5. Reconstruct the transport heatmaps -(mid-circuit-observables)= +The following panels use the analog guide's time window, noise strengths, and +shared square-root color scale. They show absolute occupation, without +normalizing by the remaining excitation. -```{note} -This section uses `num_traj=64` during the documentation build. Increase -`num_traj` locally for lower-variance layer curves. +```{code-cell} python +:tags: [hide-input] +import matplotlib.pyplot as plt +from matplotlib.colors import PowerNorm +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", + "font.serif": ["STIXGeneral"], + "mathtext.fontset": "stix", + "font.size": 11, + "axes.labelsize": 11, + "axes.linewidth": 0.7, + "xtick.labelsize": 10, + "ytick.labelsize": 10, + "xtick.direction": "in", + "ytick.direction": "in", + "xtick.top": True, + "ytick.right": True, + "legend.fontsize": 9, + "legend.frameon": False, + "lines.linewidth": 1.6, + "figure.constrained_layout.use": True, + "savefig.dpi": 180, +}) +fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.4), sharex=True, sharey=True) +for ax, values, rate, panel in zip(axes.flat, occupations, rates, "abcd", strict=True): + image = ax.pcolormesh(times, np.arange(length), values, shading="auto", + cmap="cividis", norm=PowerNorm(0.5, vmin=0, vmax=1), + rasterized=True) + label = "Noiseless" if rate == 0 else rf"$\gamma={rate:g}$" + ax.set_title(f"({panel}) {label}", loc="left", fontsize=11) + ax.set(xticks=[0, 1, 2, 3], yticks=[0, 5, 10, 15, 19]) +for ax in axes[-1]: + ax.set_xlabel(r"Represented time $t$") +for ax in axes[:, 0]: + ax.set_ylabel(r"Site $i$") +fig.colorbar(image, ax=axes.ravel().tolist(), + label=r"Occupation $\langle n_i\rangle$", ticks=[0, 0.1, 0.5, 1]) +plt.show() ``` -Set `sample_layers=True` on -{class}`~mqt.yaqs.core.data_structures.simulation_parameters.DigitalSimParams` -and insert barriers labelled `SAMPLE_OBSERVABLES` (case-insensitive) where you -want measurements. YAQS records observables at the circuit start, after each -labelled barrier, and after the final gate layer. +**Digital reconstruction of excitation transport and loss.** Exchange gates +spread the initial excitation along the chain, while increasing relaxation +suppresses occupation at later times. The broad patterns reproduce the analog +example, with differences from Trotter splitting and finite trajectory sampling. -The example below starts from $\ket{+}^{\otimes n}$, applies a chain of $R_{ZZ}$ -entanglers, and tracks how amplitude damping gradually drives each -$\langle Z_i \rangle$ toward $+1$. Only barriers labelled `SAMPLE_OBSERVABLES` -trigger sampling; unlabelled barriers are ignored. +The heatmaps alone do not tell us how close the circuit is to Hamiltonian +evolution. To separate the approximation in the gates from sampling noise, we +next compare the noiseless circuit with an analog reference and a finer circuit. -```{code-cell} ipython3 -from qiskit.circuit import QuantumCircuit +## 6. Compare with analog evolution -layer_qubits = 5 -qc = QuantumCircuit(layer_qubits) +Run the same Hamiltonian without noise, then halve the circuit step while +keeping the total duration fixed. Select every second checkpoint of the finer +circuit to compare at the original sampling times. -for segment in range(6): - for i in range(layer_qubits - 1): - qc.rzz(0.7, i, i + 1) - if segment < 5: - qc.barrier(label="SAMPLE_OBSERVABLES") +```{code-cell} python +from mqt.yaqs import AnalogSimParams, Hamiltonian -noise_factor = 0.1 -layer_noise = NoiseModel([ - {"name": "lowering", "sites": [i], "strength": noise_factor} for i in range(layer_qubits) -]) - -layer_state = State(layer_qubits, initial="x+", pad=16) -layer_params = DigitalSimParams( - observables=[Observable("z", i) for i in range(layer_qubits)], - num_traj=64, - sample_layers=True, - max_bond_dim=12, +hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) +analog_params = AnalogSimParams( + observables=observables, elapsed_time=steps * dt, dt=dt, preset="fast", ) +analog = sim.run(state, hamiltonian, analog_params) +analog_occupation = (1 - np.asarray(analog.expectation_values)) / 2 -layer_result = sim.run(layer_state, qc, layer_params, layer_noise) -layer_traj = np.vstack([np.real(v) for v in layer_result.expectation_values]) - -fig, ax = plt.subplots(figsize=(8, 4), layout="constrained") -depth = np.arange(layer_traj.shape[1]) -qubit_labels = [rf"$q_{i}$" for i in range(layer_qubits)] -im = ax.imshow( - layer_traj, - aspect="auto", - origin="lower", - vmin=-1, - vmax=1, - extent=(-0.5, layer_traj.shape[1] - 0.5, -0.5, layer_qubits - 0.5), -) -ax.set_xlabel("Sampling index") -ax.set_ylabel("Qubit") -ax.set_xticks(depth) -ax.set_yticks(range(layer_qubits), qubit_labels) -ax.set_title(r"Mid-circuit $\langle Z \rangle$ under damping") -fig.colorbar(im, ax=ax, shrink=0.9, label=r"$\langle Z \rangle$") +fine_circuit, _ = xy_circuit(length, dt / 2, steps * 2) +fine_result = sim.run(state, fine_circuit, params) +fine_occupation = (1 - np.asarray(fine_result.expectation_values).real[:, ::2]) / 2 ``` -The checkpoint index is a circuit-analysis coordinate rather than physical time. -Standalone digital results leave `result.times` as `None`; plot against -`np.arange(len(values))` for circuit depth. In a digital–analog -`SimulationProgram`, digital checkpoints are placed on `result.times` at their -segment's physical-time offset, preserving order as coincident samples. +The first panel compares final occupation profiles. The second sums occupation +over sites and compares noisy circuit estimates with the analog model's decay +law, $N(t)=e^{-\gamma t}$. This law holds because the initial state contains one +excitation, the Hamiltonian conserves excitation, and relaxation is uniform. + +```{code-cell} python +:tags: [hide-input] +fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.2)) +ax = axes[0] +for values, label, style, color in ( + (analog_occupation, "Analog", "-", "0.2"), + (occupations[0], r"Circuit $\Delta t=0.25$", "o", "#0072B2"), + (fine_occupation, r"Circuit $\Delta t=0.125$", "+", "#D55E00"), +): + ax.plot(np.arange(length), values[:, -1], style, color=color, + label=label, markersize=4) +ax.set(xlabel="Site", ylabel=r"Final occupation $\langle n_i\rangle$", + xticks=[0, 5, 10, 15, 19]) +ax.set_title("(a) Noiseless transport", loc="left", fontsize=11) +ax.legend(loc="upper center", fontsize=8) + +ax = axes[1] +colors = ["0.2", "#0072B2", "#009E73", "#D55E00"] +fine_times = np.linspace(0, times[-1], 200) +for rate, values, color in zip(rates, occupations, colors, strict=True): + ax.plot(times, values.sum(axis=0), "o", color=color, markersize=3, + label="Noiseless" if rate == 0 else rf"$\gamma={rate:g}$") + ax.plot(fine_times, np.exp(-rate * fine_times), "--", color=color, linewidth=1) +ax.set(xlabel=r"Represented time $t$", ylabel=r"Total excitation $N(t)$", + xlim=(0, 3), ylim=(0, 1.08), xticks=[0, 1, 2, 3]) +ax.set_title("(b) Excitation survival", loc="left", fontsize=11) +ax.legend(ncols=2, loc="upper right", bbox_to_anchor=(1, 0.9), fontsize=8) +plt.show() +``` + +**Analog and digital transport compared at the same duration.** The final +noiseless profiles agree closely, and a smaller Trotter step reduces the +circuit's splitting error. Noisy survival estimates follow the uniform-loss +decay within the resolution of this 16-trajectory example; dashed lines show the +continuous-model reference. + +This comparison connects the two workflows through their physical model, rather +than equating a circuit's gate count with elapsed time. Check Trotter-step +convergence separately from MPS tolerances and trajectory uncertainty. For noisy +refinement, rebuild the circuit and rescale the strengths using the new step and +gate exposures. Finite-step noise splitting can also affect the spatial profile; +agreement of total excitation alone does not validate that profile. -## 4. OpenQASM inputs +## 7. OpenQASM inputs Pass an OpenQASM 2 source string (or file path) directly to {meth}`~mqt.yaqs.Simulator.run` instead of building a @@ -235,7 +329,7 @@ OpenQASM 3 requires `uv pip install mqt-yaqs[qasm3]`. {class}`~mqt.yaqs.EquivalenceChecker` accepts the same path and string forms; see {doc}`equivalence_checking`. -## 5. Gate application modes +## 8. Gate application modes `DigitalSimParams.gate_mode` selects how two-qubit gates are applied to the MPS. The default `"mpo"` uses extended gate MPOs for long-range pairs; `"tdvp"` uses @@ -265,7 +359,7 @@ for mode in ("mpo", "tdvp"): print({mode: round(value, 4) for mode, value in z0_by_mode.items()}) ``` -## 6. Related topics +## Related topics - {doc}`digital_analog_simulation` — combine digital operations with analog evolution in one program From 69938a13592d569736a200f366a793056936e6de Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:20:28 +0200 Subject: [PATCH 10/30] updated circuit verification --- docs/examples/equivalence_checking.md | 583 ++++++++++++-------------- docs/index.md | 2 +- 2 files changed, 275 insertions(+), 310 deletions(-) diff --git a/docs/examples/equivalence_checking.md b/docs/examples/equivalence_checking.md index 6b8c8ec81..38aaf6788 100644 --- a/docs/examples/equivalence_checking.md +++ b/docs/examples/equivalence_checking.md @@ -2,360 +2,325 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true execution_timeout: 300 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - -# Equivalence Checking - -YAQS can test whether two quantum circuits implement the same unitary map, up to -a **global phase** and numerical tolerance. The public API is -{class}`~mqt.yaqs.EquivalenceChecker`, which forms the composed operator -$W = U_1 U_2^\dagger$ from the two circuits and checks whether $W$ is close to -the identity. - -For most workflows—comparing a high-level circuit to a transpiled variant, -regression tests on compiled circuits, or checking compiler passes—the -**MPO backend** (`representation="mpo"`) is the intended tool. It scales to -larger qubit counts via tensor-network updates and SVD truncation controlled by -`threshold`. The **matrix backend** (`representation="matrix"`) is a dense, -tensorized reference useful on very small circuits; both backends target the -same equivalence criterion. - -## Choosing a backend +# Circuit Verification -| Backend | When to use | Scaling | Numerical knobs | -| ----------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------- | -| **`mpo`** (recommended) | Default for real circuits; long-range gates; anything beyond a handful of qubits | Polynomial in qubits for many structured circuits; memory grows with bond dimension | `threshold` (SVD truncation), `fidelity` | -| **`matrix`** | Small-circuit checks, debugging, cross-checking the MPO path | Exponential in qubits ($4^n$ complex numbers for the dense operator tensor) | `fidelity` only | -| **`auto`** | Convenience: picks matrix for `num_qubits <= matrix_max_qubits`, otherwise MPO | Same as the selected backend | Both when MPO is selected | +A circuit must use the gates and connections that a quantum device supports. +Transpilation makes those changes, but the compiled circuit should still perform +the intended operation. We first verify a circuit compiled for a hardware +target, then introduce a rotation-angle bug and ask how hardware noise changes +its agreement with the original circuit. -```{note} -`representation="auto"` remains the constructor default, but -**you should pass `representation="mpo"` explicitly** when equivalence checking -is part of a pipeline you care about. Auto only avoids thinking about backend -choice on tiny circuits; it does not change the fact that MPO is the primary -algorithm in YAQS. -``` - -With the default cutover of **7** qubits (`matrix_max_qubits` on -{class}`~mqt.yaqs.EquivalenceChecker`), auto uses the matrix backend only for -circuits with **at most seven qubits**. From eight qubits upward, auto selects -MPO. Override the cutover with `matrix_max_qubits` if needed. +This extends the circuit comparison in {doc}`quickstart`. The four-qubit example +uses the standard YAQS installation and Matplotlib for plotting, without a +hardware account. Run the cells in order in a notebook; for a script, use the +entry-point guard in {doc}`simulator_initialization`. -## What “equivalent” means - -Two circuits $C_1$ and $C_2$ on $n$ qubits are reported as equivalent when their -unitaries $U_1$ and $U_2$ satisfy - -```{math} -U_1 U_2^\dagger \approx e^{i\phi}\, I -``` +## 1. Compile for hardware constraints -for some global phase $\phi$, within `fidelity`. On the **matrix** path, only -**final** measurements are stripped before building $U$; mid-circuit -measurements raise an error. Barriers are ignored on the matrix path. The -**MPO** backend walks circuit DAGs directly (measurements and barriers are -skipped during zone extraction); mid-circuit measurements are not supported for -unitary equivalence on either backend. Gates on more than two qubits (for -example `ccx`) are supported on the matrix backend only; the MPO backend rejects -them with a `ValueError`. Unknown unitaries translate via the matrix fallback, -which supports at most eight qubits (see {doc}`custom_gates`). See -{cite:p}`sander2025_EquivalenceChecking` for the underlying MPO method. - -A noiseless `check` returns a dictionary: - -| Key | Type | Meaning | -| --------------------------------- | ------------------- | ------------------------------------------------------------------------- | -| `equivalent` | `bool` | Whether the circuits pass the identity test | -| `fidelity` | `float` | Measured normalized overlap of $W=U_1U_2^\dagger$ with the identity | -| `elapsed_time` | `float` | Wall time in seconds | -| `representation` | `str` | `"matrix"` or `"mpo"` — which backend ran | -| `matrix` | `ndarray` or `None` | Dense composed operator $W$ as a $(2^n, 2^n)$ matrix; matrix backend only | -| `mpo` | `MPO` or `None` | Composed operator on the MPO backend; `None` on matrix | -| `schmidt_values` | `ndarray` or `None` | Center-cut operator Schmidt values (`length // 2`); MPO backend only | -| `center_cut_entanglement_entropy` | `float` or `None` | Operator entanglement entropy at `length // 2`; MPO backend only | -| `global_entanglement_entropy` | `float` or `None` | Sum of operator entanglement entropies over internal bonds; MPO only | - -## Parameters - -{class}`~mqt.yaqs.EquivalenceChecker` stores settings on the instance; circuits -are passed to {meth}`~mqt.yaqs.EquivalenceChecker.check` each time. - -- **`threshold`** (default `1e-13`): singular-value cutoff during MPO updates. - Smaller values retain more bond dimension and are stricter; larger values - speed up checks at the cost of accuracy. -- **`fidelity`** (default `1 - 1e-13`): minimum normalized overlap between $W$ - and the identity (global phase removed). It must be finite and between `0` and - `1`. Noiseless and noisy results use this same scale and threshold. -- **`representation`**: `"mpo"`, `"matrix"`, or `"auto"`. -- **`matrix_max_qubits`** (default **7**): only affects `"auto"`. -- **`parallel`** (default `True`): enables checkerboard **MPO** pair updates in - a **thread pool** from 12 qubits upward and allows noisy trajectories to use a - **process pool**. Set it to `False` to keep either path serial. -- **`max_workers`** (default `None`): cap on worker threads when `parallel=True` - (noiseless MPO checks), and on worker processes for noisy ensembles. Worker - counts also respect the available CPUs and number of trajectories. -- **`mp_context`**: start method for noisy-ensemble process pools (`"auto"`, - `"fork"`, `"spawn"`) when one is created. Noiseless MPO zone parallelism - inside `iterate()` still uses in-process threads. - -```{code-cell} ipython3 -from mqt.yaqs import EquivalenceChecker +Our circuit entangles qubit 0 with every other qubit, then applies local +rotations. A device with a line of nearest-neighbor connections cannot execute +all three controlled-X gates directly. The transpiler must route the circuit and +express its rotations in the device's native gate set. -# Recommended: MPO for the circuits you care about -mpo_checker = EquivalenceChecker( - representation="mpo", - threshold=1e-6, - fidelity=1 - 1e-13, +```{code-cell} python +import numpy as np +from qiskit import QuantumCircuit, transpile +from qiskit.providers.fake_provider import GenericBackendV2 + +num_qubits = 4 +original = QuantumCircuit(num_qubits) +original.h(0) +for site in range(1, num_qubits): + original.cx(0, site) +for site in range(num_qubits): + original.ry(0.3 * (site + 1), site) + +connections = [[0, 1], [1, 0], [1, 2], [2, 1], [2, 3], [3, 2]] +backend = GenericBackendV2( + num_qubits, + basis_gates=["rz", "sx", "x", "cx"], + coupling_map=connections, + noise_info=False, +) +compiled = transpile( + original, + backend=backend, + initial_layout=list(range(num_qubits)), + optimization_level=1, + seed_transpiler=7, ) -# Auto: matrix if num_qubits <= 7, else MPO -auto_checker = EquivalenceChecker(representation="auto") +print("Original gates:", dict(original.count_ops())) +print("Compiled gates:", dict(compiled.count_ops())) ``` -## Loading from OpenQASM +`GenericBackendV2` supplies an **offline hardware target**, not measured device +data. To compile for a real device, pass that device's Qiskit backend instead. +The native gates and connectivity then come from its target. YAQS does not +import the backend's calibration data into a noise model; we define the noise +separately below. -{meth}`~mqt.yaqs.EquivalenceChecker.check` accepts OpenQASM 2 and OpenQASM 3 -inputs directly — no need to call Qiskit's loaders first. Pass a filesystem -path, a `pathlib.Path`, or a raw OpenQASM string (when the first substantive -line declares `OPENQASM`): +## 2. Align the outputs and verify the compiled circuit -```python -checker = EquivalenceChecker(representation="mpo") +Routing can leave logical outputs on different physical qubits. The checker +compares circuit wires directly, so we must account for this mapping before +interpreting a mismatch as a compiler bug. We fixed the initial placement to +`[0, 1, 2, 3]`; the remaining change is the final output permutation. -# File paths (preferred when the program uses include directives) -result = checker.check("original.qasm", "transpiled.qasm") +Append that permutation to the **reference** circuit. The compiled circuit stays +as the device would execute it, including its routing gates. Qiskit's +`PermutationGate` lists the input wire for each output position, so we invert +the logical-to-physical map returned by `final_index_layout()`. -# Raw source strings -result = checker.check(qasm_source_a, qasm_source_b) -``` - -OpenQASM 3 requires the optional package `qiskit-qasm3-import` -(`uv pip install mqt-yaqs[qasm3]`). The same path and string forms work with -{meth}`~mqt.yaqs.Simulator.run` for circuit simulation. +```{code-cell} python +from qiskit.circuit.library import PermutationGate -## Example: compare original and transpiled circuits +from mqt.yaqs import EquivalenceChecker -The workflow below builds a parameterized circuit, transpiles it to another gate -set, and checks equivalence with the **MPO backend**. This matches typical -compiler-verification use cases. +output_mapping = compiled.layout.final_index_layout() +reference = original.copy() +reference.append(PermutationGate(np.argsort(output_mapping).tolist()), range(num_qubits)) +reference = reference.decompose(gates_to_decompose=["permutation"]) -Define the number of qubits and circuit depth. +checker = EquivalenceChecker() +verified = checker.check(reference, compiled) -```{code-cell} ipython3 -num_qubits = 5 -depth = num_qubits +print("Logical output → physical qubit:", output_mapping) +print("Equivalent:", verified["equivalent"]) +print(f"Overlap: {verified['fidelity']:.12f}") ``` -Create a TwoLocal circuit and decompose it. +For the reference unitary $U$ and compiled unitary $V$, the returned overlap is -```{code-cell} ipython3 -from qiskit.circuit.library.n_local import TwoLocal +$$ +a=\frac{|\operatorname{Tr}(UV^\dagger)|}{2^n}. +$$ -import numpy as np - -circuit = TwoLocal(num_qubits, ["rx"], ["rzz"], entanglement="linear", reps=depth).decompose() -num_pars = len(circuit.parameters) -rng = np.random.default_rng() -values = rng.uniform(-np.pi, np.pi, size=num_pars) -circuit.assign_parameters(values, inplace=True) -circuit.measure_all() -``` +An overlap of one means that the circuits agree on every input state, up to a +global phase. `equivalent` tests whether this value reaches the checker's +`fidelity` setting, which defaults to `1 - 1e-13`. The compiled circuit passes +this numerical check. This compares the full operation, rather than only the +output from one chosen input state. -Transpile the circuit to a new basis. - -```{code-cell} ipython3 -from qiskit import transpile - -basis_gates = ["cz", "rz", "sx", "x", "id"] -transpiled_circuit = transpile(circuit, basis_gates=basis_gates, optimization_level=1) -``` +The default checker selects its backend automatically: dense matrices for at +most seven qubits, and a matrix product operator (MPO) for larger circuits. This +small example therefore uses the matrix backend. -Run equivalence checking with the MPO backend. - -```{code-cell} ipython3 -from mqt.yaqs import EquivalenceChecker - -checker = EquivalenceChecker(representation="mpo", threshold=1e-6, fidelity=1 - 1e-13) -result = checker.check(circuit, transpiled_circuit) +```{note} +The mapping above assumes the identity initial placement and equal circuit +widths. For another initial layout or a backend that adds ancillas, also align +the input wires and the circuit widths before checking. YAQS does not apply +Qiskit's transpilation layout automatically. ``` -The same pair with `representation="auto"` on this five-qubit example selects -the matrix backend because $5 \leq 7$. For a consistent pipeline, keep -`representation="mpo"` as above. - -```{code-cell} ipython3 -auto_result = EquivalenceChecker(representation="auto").check(circuit, transpiled_circuit) -``` +## 3. Introduce a rotation-angle bug -## Matrix backend (small circuits) +Suppose a compiler pass changes one native $R_z$ angle by $\delta$. We copy the +compiled circuit and change its first `rz` instruction, leaving the routing and +all other gates intact. -The matrix backend builds $W = U_1 U_2^\dagger$ as a tensor with $2n$ indices of -dimension 2 and applies local gate contractions. It uses the same trace-based -identity test as the MPO path. Memory and time grow as $\mathcal{O}(4^n)$, so -this backend is practical only for very small $n$. +```{code-cell} python +def with_angle_error(circuit, delta): + """Offset the first native Z rotation by delta radians.""" + changed = circuit.copy() + index = next(i for i, instruction in enumerate(changed.data) if instruction.operation.name == "rz") + instruction = changed.data[index] + rotation = instruction.operation.copy() + rotation.params[0] += delta + changed.data[index] = instruction.replace(operation=rotation) + return changed -Use it when: -- You want a dense reference on at most a few qubits. -- You are debugging the equivalence machinery itself. +angles = np.linspace(0, np.pi, 25) +angle_overlaps = [checker.check(reference, with_angle_error(compiled, delta))["fidelity"] for delta in angles] +bug_angle = np.pi / 2 +buggy = with_angle_error(compiled, bug_angle) +bug_result = checker.check(reference, buggy) -```python -small_checker = EquivalenceChecker(representation="matrix", fidelity=1 - 1e-13) +print("Buggy circuit equivalent:", bug_result["equivalent"]) +print(f"Overlap with a π/2 angle error: {bug_result['fidelity']:.4f}") ``` -Forcing `representation="matrix"` on large circuits is allowed but can exhaust -memory; prefer MPO instead. - -## Parallel execution - -Set `parallel=True` on {class}`~mqt.yaqs.EquivalenceChecker` to speed up **MPO** -checks on circuits where many independent updates can run at once. This is the -default; below 12 qubits the implementation keeps a single noiseless check -serial even when `parallel=True`, because thread overhead would dominate. A -single matrix check is also serial. When a noise model is supplied, independent -matrix or MPO trajectories can instead run across processes. - -Within each checkerboard sweep, disjoint nearest-neighbor pairs update different -MPO site tensors and can be computed in parallel in a shared thread pool (one -pool per `iterate()` call). Temporal zones are still extracted from the DAGs -serially; only the tensor contraction and SVD step runs concurrently. Long-range -gate handling stays serial in this version. - -```{code-cell} ipython3 -wide_checker = EquivalenceChecker( - representation="mpo", - max_workers=4, -) -``` +For this single rotation error, the overlap is exactly $|\cos(\delta/2)|$ in +exact arithmetic. The other gates cancel inside the trace, so the formula does +not depend on where the faulty rotation occurs. A $\pi/2$ error gives an overlap +of about 0.707 and fails the equivalence check. A smaller error also fails once +its overlap falls below the chosen tolerance. (equivalence-noise-model)= -## Comparing with a noise model - -Passing a {class}`~mqt.yaqs.NoiseModel` asks how close a -**compiled, hardware-like** circuit remains to an **ideal** specification under -sampled noise. Each trajectory materializes a stochastic realization of the -noise model on the compiled circuit. This gives a Monte Carlo comparison rather -than an exact noisy-channel equivalence certificate. - -The checker rejects noise processes that it cannot materialize as stochastic -circuit operations; those remain available through the simulator. - -Noise is sampled onto the **second** circuit argument only. A supported -two-qubit unitary gate is a noise opportunity; single-qubit gates, gates on -three or more qubits, barriers, and measurements are not. A process is eligible -when its complete site support is contained in the gate support. The selected -equivalence backend must also support the original gate. - -Within `EquivalenceChecker`, each resolved `strength` is a dimensionless branch -probability $p_i$ at every eligible gate. Processes with the same exact site -support form one categorical draw: process $i$ occurs with probability $p_i$, -and no process from that support occurs with probability $1-\sum_i p_i$. -Consequently, each same-support sum must be at most one. Different exact -supports are sampled independently, so multiple errors may follow one gate. This -also applies to overlapping supports such as `[0]` and `[0, 1]`, which may both -be selected in one trajectory. - -For an isotropic Pauli error with total probability $p$ on each gate qubit, -assign `strength=p/3` to X, Y, and Z on that one-qubit support. The identity -then has probability $1-p$ on each qubit, and the per-qubit draws are -independent. - -Writing $U_{\mathrm{ideal}}$ for the first circuit and $U_{\mathrm{noisy},r}$ -for trajectory $r$ of the second, the relative operator has the order - -```{math} -Q_r = U_{\mathrm{ideal}} U_{\mathrm{noisy},r}^\dagger. -``` - -If $a_r = |\operatorname{Tr}(Q_r)| / d$ is the normalized root overlap, the -sampled channel's process fidelity $F_{\mathrm{pro}}=\mathbb E[a_r^2]$ is -estimated internally by +## 4. Add a hardware noise model -```{math} -\widehat F_{\mathrm{pro}} = \frac{1}{N}\sum_{r=1}^{N} a_r^2. -``` +A correctly compiled circuit can still deviate from the intended operation +because its gates are noisy. We model a Pauli error after each controlled-X +gate: on each participating qubit, apply $X$, $Y$, or $Z$ with probability $p/3$ +each, and apply no error with probability $1-p$. Sweeping $p$ separates the +noiseless compiler check from the effect of executing the circuit on noisy +hardware. -The public result stays on the noiseless scale: +```{code-cell} python +from mqt.yaqs import NoiseModel -```{math} -\mathtt{fidelity}=\sqrt{\widehat F_{\mathrm{pro}}}. +probabilities = np.array([0.0, 0.005, 0.015, 0.03, 0.06, 0.1]) +implementations = {"Correct compilation": compiled, "Rotation-angle bug": buggy} +noise_results = {label: [] for label in implementations} + +for probability in probabilities: + noise = NoiseModel([ + {"name": f"pauli_{axis}", "sites": [site], "strength": float(probability / 3)} + for site in range(num_qubits) + for axis in "xyz" + ]) + for label, circuit in implementations.items(): + result = checker.check( + reference, + circuit, + noise_model=noise, + num_traj=256, + random_seed=7, + ) + noise_results[label].append(result) ``` -For $N>1$, `fidelity_error` is the approximate delta-method Monte Carlo standard -error on this root-fidelity scale. It is `0.0` when every sampled overlap is -zero and `None` for one trajectory. - -Apply noise to the transpiled circuit from the earlier example. The noisy call -returns the same primary fields as the noiseless call: - -```{code-cell} ipython3 -from mqt.yaqs import NoiseModel - -checker = EquivalenceChecker(representation="mpo", threshold=1e-6) -noiseless = checker.check(circuit, transpiled_circuit) -noise = NoiseModel([ - {"name": "pauli_x", "sites": [qubit], "strength": 0.02} for qubit in range(num_qubits) -]) -noisy = checker.check( - circuit, - transpiled_circuit, - noise_model=noise, - num_traj=24, - random_seed=0, -) +**Noise acts on the second circuit only.** Every supported two-qubit unitary is +a noise opportunity when it contains all sites of a process. Thus routing gates +also contribute noise. Single-qubit gates, barriers, measurements, and gates on +three or more qubits do not create noise opportunities. + +For the checker, `strength` is a +**dimensionless probability per eligible gate**, not a Lindblad rate or a +probability for the whole circuit. This differs from noise strengths in +{doc}`analog_simulation` and {doc}`circuit_observables`. Processes with the same +exact support are mutually exclusive, and their probabilities must sum to at +most one. Different supports are sampled independently, including overlapping +supports. + +Each trajectory gives a unitary $V_r$ with sampled Pauli errors. The noisy +result reports the square root of the estimated process fidelity, + +$$ +\mathtt{fidelity}=\sqrt{\frac{1}{N}\sum_{r=1}^{N} +\left|\frac{\operatorname{Tr}(UV_r^\dagger)}{2^n}\right|^2}. +$$ + +This is the root-mean-square trajectory overlap, not the mean overlap or a +measurement success probability. `fidelity_error` estimates its Monte Carlo +standard error. Increasing `num_traj` reduces sampling uncertainty; it does not +make the assumed hardware model more accurate. + +## 5. Compare the bug and noise effects + +The left panel checks the controlled rotation error against its exact formula. +The right panel compares correct and faulty compilations under the same noise +model. Plotting code is folded so the verification workflow remains visible. + +```{code-cell} python +:tags: [hide-input] +import matplotlib.pyplot as plt + +plt.rcParams.update({ + "font.family": "serif", + "font.serif": ["STIXGeneral"], + "mathtext.fontset": "stix", + "font.size": 10, + "axes.labelsize": 11, + "axes.linewidth": 0.8, + "xtick.direction": "in", + "ytick.direction": "in", + "xtick.top": True, + "ytick.right": True, + "svg.fonttype": "none", +}) +%config InlineBackend.figure_formats = ['svg'] -print(f"noiseless: equivalent={noiseless['equivalent']}, fidelity={noiseless['fidelity']:.6f}") -print( - "noisy: " - f"sample threshold passed={noisy['equivalent']}, " - f"root process fidelity={noisy['fidelity']:.4f} " - f"+/- {noisy['fidelity_error']:.4f}" -) -print(f"trajectories: {noisy['num_traj']}") +colors = ["#225c80", "#bb563b"] +fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.8), sharey=True, layout="constrained") +fine_angles = np.linspace(0, np.pi, 200) +axes[0].plot(fine_angles / np.pi, np.abs(np.cos(fine_angles / 2)), color="0.55", lw=1.7, label=r"$|\cos(\delta/2)|$") +axes[0].plot(angles / np.pi, angle_overlaps, "o", ms=3.5, color=colors[0], label="YAQS") +axes[0].plot(0.5, bug_result["fidelity"], "D", ms=5, color=colors[1], label=r"Bug: $\delta=\pi/2$") +axes[0].set(xlabel=r"Rotation-angle error $\delta/\pi$", ylabel="Root process fidelity", xlim=(-0.03, 1.03)) +axes[0].legend(frameon=False, fontsize=9, loc="lower left") + +for (label, results), color in zip(noise_results.items(), colors, strict=True): + values = [result["fidelity"] for result in results] + errors = [result["fidelity_error"] for result in results] + axes[1].errorbar(100 * probabilities, values, yerr=errors, color=color, marker="o", ms=4, lw=1.6, capsize=2.5, label=label) +axes[1].set(xlabel=r"Pauli-error probability per gate qubit $p$ (%)", xlim=(-0.3, 10.3)) +axes[1].legend(frameon=False, fontsize=9, loc="lower left") +for label, ax in zip(("(a)", "(b)"), axes, strict=True): + ax.text(0.02, 1.03, label, transform=ax.transAxes, va="bottom", fontweight="bold") + ax.set_ylim(-0.04, 1.06) + ax.spines[["top", "right"]].set_visible(False) + ax.tick_params(top=False, right=False) +plt.show() ``` -Here `strength=0.02` means a 2% X-error probability on that qubit after each -eligible two-qubit gate containing it. It is neither a Lindblad rate nor a 2% -error probability for the complete circuit. - -A noisy `check` returns -{class}`~mqt.yaqs.equivalence_checker.EquivalenceEnsembleResult` with the same -fields as a noiseless check, plus `fidelity_error` and `num_traj`. Noisy -`equivalent` compares the point estimate with `checker.fidelity`; it does not -use the error bar and is not an exact certificate. For MPO checks, entropies are -trajectory means and `schmidt_values` is the zero-padded mean trajectory -spectrum, not a channel spectrum. `matrix` and `mpo` are `None` because the -ensemble is a channel rather than one relative unitary. Pass -`return_trajectories=True` to include the individual trajectory results. - -Distribution-valued strengths are resolved once per `check` call, so every -trajectory uses the same resolved probabilities. The checker then validates the -same-support sums; an out-of-range draw raises `ValueError`. A nonnegative -`random_seed` makes the sampled ensemble reproducible independently of process -scheduling. - -## Performance notes - -Internal benchmarks (`benchmarks/bench_equivalence_matrix_vs_mpo.py`) on random -`EfficientSU2` circuits show the matrix backend winning only at very small qubit -counts; MPO is faster from roughly eight qubits upward on those workloads. That -aligns with the default auto cutover at seven qubits: auto uses matrix only -where it is still affordable, and MPO for everything larger. - -## Related topics - -- {doc}`realistic_noise_models` — Pauli and dissipative process names, disorder -- {doc}`custom_gates` — Qiskit translation, matrix fallback, and TDVP generators -- {doc}`simulator_initialization` — running simulations with - {class}`~mqt.yaqs.Simulator` -- {doc}`simulation_parameters` — presets and truncation for **simulation** - (separate from equivalence `threshold`) +**Compiler errors and hardware noise reduce agreement in different ways.** (a) +The noiseless overlap follows the single-angle error formula. (b) With no noise, +the correct compilation agrees with the reference, while the faulty circuit +starts below one. As the Pauli-error probability increases, both estimates fall +in this example. Error bars show one Monte Carlo standard error from 256 +trajectories. Lines join sampled points. + +This comparison tells us whether compilation preserves the intended operation +and how a specified hardware model changes its process fidelity. It does not +identify an unknown bug or infer device noise from measurements. For learning +noise strengths from observed dynamics, see {doc}`digital_twin`. + +For noisy results, `equivalent` applies the same overlap threshold to the point +estimate without using its error bar. Treat this as a sampled comparison, not an +exact noisy-channel certificate. In particular, a zero reported error bar when +every sampled overlap is zero does not establish zero uncertainty. + +## Further options + +### Accuracy and backends + +Keep `representation="auto"` for automatic selection, or choose `"matrix"` or +`"mpo"` explicitly. Dense matrix storage grows as $4^n$; MPO cost depends on the +operator's bond dimensions and can also grow rapidly. The MPO method is +described in {cite:p}`sander2025_EquivalenceChecking`. + +The constructor's `fidelity` sets the decision threshold, while `threshold` sets +the MPO singular-value cutoff. These control different errors. For an +approximate comparison, choose a decision threshold that matches your purpose +and check that numerical truncation does not determine the answer. The returned +`representation` records which backend ran. + +Terminal measurements are ignored for unitary checks; mid-circuit measurements +are unsupported. Decompose gates on more than two qubits before using the MPO +backend. See {doc}`custom_gates` for supported gate translation. + +### Noise models and returned data + +The checker supports stochastic Pauli errors, including Pauli products. It +rejects dissipative channels such as relaxation; use the simulator for those +channels. See {doc}`realistic_noise_models` for model construction. +Distribution-valued strengths are drawn once per `check`, then held fixed across +its trajectories. The resolved same-support probabilities must still sum to at +most one. + +Pass `return_trajectories=True` to keep each trajectory result. Noisy results +have `matrix=None` and `mpo=None` because the ensemble is a channel. For MPO +checks, returned entropies and zero-padded Schmidt spectra are trajectory means, +not spectra of that channel. `fidelity_error` is `None` for a single trajectory. + +### Inputs and execution + +`check` accepts Qiskit circuits, OpenQASM file paths, and raw OpenQASM source +strings. OpenQASM 3 requires the optional `mqt-yaqs[qasm3]` extra. File paths +allow includes to resolve relative to the source file. + +Parallel execution is enabled by default. `max_workers` caps concurrency, and +`mp_context` controls the start method for noisy process pools. A nonnegative +`random_seed` makes sampled trajectories reproducible across worker scheduling. +See {class}`~mqt.yaqs.EquivalenceChecker` for the full settings and returned +fields. diff --git a/docs/index.md b/docs/index.md index cef4bcba3..63381ac4b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -120,7 +120,7 @@ Analog-digital simulation Environmental memory Noise characterization -Circuit equivalence +Circuit verification ``` ```{toctree} From 8a7bdc8bf1f38a635d072a0b6764fcfa3831ad5b Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:38:32 +0200 Subject: [PATCH 11/30] updated examples --- docs/examples/characterization.md | 635 ++++++++++++++++-------------- docs/examples/digital_twin.md | 499 ++++++++++++----------- docs/examples/memory_surrogate.md | 584 ++++++++++++++------------- 3 files changed, 921 insertions(+), 797 deletions(-) diff --git a/docs/examples/characterization.md b/docs/examples/characterization.md index 1b1e3ac2b..597606d1b 100644 --- a/docs/examples/characterization.md +++ b/docs/examples/characterization.md @@ -2,354 +2,395 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true - execution_timeout: 900 + execution_timeout: 300 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Probing Environmental Memory -Open quantum systems in YAQS couple a **probe qubit** (site 0) to an -**environment** simulated by the remaining chain. **Environmental memory** -measures how long the environment keeps past control and measurement choices -relevant for future probe responses, evaluated at a temporal cut $c$ in a -sequence of interventions. +A quantum system can leave information in its environment and encounter that +information again later. To study this memory, we control one probe qubit, +interrupt its evolution with a measurement and preparation, then ask whether its +future responses still depend on the past. The interruption removes the probe's +direct link to its earlier state while the environment keeps evolving. -Memory characterization currently supports qubit Hamiltonians only. +We extend the coupling sweep in {doc}`quickstart` to explain the probing +schedule, the response matrix, and its spectrum. We then test memory persistence +under repeated resets and add dephasing in a short process-tensor example. The +examples use the standard YAQS installation and Matplotlib for plotting. Run the +cells in order in a notebook; for a script, use the entry-point guard in +{doc}`simulator_initialization`. -Use {meth}`~mqt.yaqs.memory_characterizer.MemoryCharacterizer.characterize` to -probe **operational memory**: assemble the **response matrix** $V(c)$, then read -$S_V(c)$, $R(c)=\exp(S_V(c))$, and the mode spectrum. +## 1. Define the probe and its environment -Alternatively, build a process tensor (default: direct MPO) and call -`compute_temporal_entropy` for **temporal entanglement** $S_{PT}(c)$ of the -multi-time process itself — a distinct quantity from $S_V(c)$. For fast dynamics -under control sequences, see {doc}`memory_surrogate`. +Site 0 is our probe qubit. Sites 1 and 2 form an environment that we do not +control or measure directly. All three spins evolve under the transverse-field +Ising Hamiltonian -## Setup +$$ +H=-J(Z_0Z_1+Z_1Z_2)-g(X_0+X_1+X_2). +$$ -```{code-cell} ipython3 -import matplotlib.pyplot as plt +We fix $g=1$, use $\hbar=1$, and vary $J$. This changes both the +probe–environment coupling and the bond within the environment. At $J=0$ the +probe is isolated, giving a reference with no environmental memory. The default +initial state is $|000\rangle$. + +```{code-cell} python import numpy as np from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer -from mqt.yaqs.characterization.memory.shared.utils import make_zero_psi -length = 4 -ham = Hamiltonian.ising(length=length, J=1.0, g=1.0) -params = AnalogSimParams(dt=0.1, max_bond_dim=16, order=1) -mc = MemoryCharacterizer(show_progress=False) -psi0 = make_zero_psi(length) +length = 3 +couplings = np.linspace(0, 1.5, 13) +params = AnalogSimParams(elapsed_time=0.5, dt=0.5, preset="fast") +characterizer = MemoryCharacterizer(show_progress=False) ``` -Throughout, `num_interventions` is the probe-sequence length $k$ and `cut` is -the causal-break index $c$ (the break sits at step $c-1$; future legs use steps -$c+1,\ldots,k$). Use $k>1$ and an interior cut so both past and future probe -legs contribute to $V(c)$. - -## Characterize with the Hamiltonian backend - -The full chain (system + environment) is simulated for each probe sequence. This -is the reference memory metric when you have a microscopic open-system model. - -```{code-cell} ipython3 -cut, num_interventions = 4, 6 -ham_result = mc.characterize( - ham, - params, - cut=cut, - num_interventions=num_interventions, - n_pasts=8, - n_futures=8, - initial_psi=psi0, - rng=np.random.default_rng(42), -) - -sv = ham_result.singular_values(cut) -fig, axes = plt.subplots(1, 2, figsize=(8, 3)) -axes[0].semilogy(sv, "o-") -axes[0].set_xlabel("mode index") -axes[0].set_ylabel("singular value") -axes[0].set_title(r"Memory spectrum at cut $c=4$") - -v = ham_result.response_matrix(cut) -im = axes[1].imshow(np.abs(v), aspect="auto", cmap="viridis") -axes[1].set_title(r"$|V(c)|$") -axes[1].set_xlabel("history index") -axes[1].set_ylabel("future probe and response channel") -fig.colorbar(im, ax=axes[1], fraction=0.046, pad=0.04) -fig.suptitle( - rf"$S_V(c={cut})={ham_result.entropy(cut):.3f}$, " - rf"$R(c)={ham_result.modes(cut):.2f}$", - y=1.02, -) -fig.tight_layout() -``` +The characterizer selects the state representation automatically: vectors for +small systems, and MPS for larger ones. Parallel execution remains enabled. The +documentation suppresses progress bars; omit `show_progress=False` to see them. +Memory characterization currently supports qubit Hamiltonians. -Use `preset="quick"`, `"balanced"`, or `"accurate"` for default probe-grid -sizes, or set `n_pasts` / `n_futures` explicitly. +(memory-theory)= -### Reading `CharacterizationResult` +## 2. Choose the probing schedule -| Access | Meaning | -| ---------------------------------- | -------------------------------------------------------------------------- | -| `result.entropy(c)` | Environmental memory entropy $S_V(c)$ | -| `result.modes(c)` | Effective memory modes $R(c)=\exp(S_V(c))$ | -| `result.singular_values(c)` | Resolution-retained spectrum used to compute $S_V(c)$ | -| `result.singular_values_full(c)` | Every compact-SVD value, including zero and unresolved tail values | -| `result.left_singular_vectors(c)` | All compact-SVD future-response directions as columns | -| `result.right_singular_vectors(c)` | All compact-SVD history-combination directions as columns | -| `result.response_matrix(c)` | $V(c)$ with $4N_f$ IXYZ future-response rows and $N_h$ history columns | -| `result.probes(c)` | Probe arrays used at cut $c$ (for reuse or inspection) | -| `result.summary()` | Human-readable table of entropies and modes | +Use four interventions separated by evolution intervals of `dt=0.5`. YAQS also +evolves before the first intervention and after the last, giving five intervals +and a total duration of 2.5. With `cut=2`, the second intervention is the +**causal break**: measure a selected outcome and prepare a new probe state. +There is one control before the break and two controls after it. -(memory-theory)= +The default `intervention_style="haar"` draws random single-qubit unitaries for +these controls. Past probes also choose the measurement at the break; future +probes choose its preparation. Keeping these choices separate lets us test +whether past settings affect the future through the environment. -## Theory: split-cut probing - -Environmental memory asks: across a grid of past and future control settings on -the probe, how many independent ways does the **environment** still correlate -past choices with accessible future responses? - -The split-cut protocol: - -1. Sample past control legs $\alpha=(U_1,\ldots,U_{c-1})$ and future legs - $\beta=(V_{c+1},\ldots,V_k)$ on the probe. -2. Insert a **causal break** at step $c$: measure on the past side and prepare - on the future side while the environment continues to evolve. -3. For each grid entry, simulate the open system, record the joint probability - of the retained outcomes and the normalized final-system Pauli response - $\mathbf{r}=(\langle I\rangle,\langle X\rangle,\langle Y\rangle,\langle Z\rangle)$. -4. Assemble the sampled response coefficients into $V(c)$. Its rows contain one - $(I,X,Y,Z)$ block per future probe, its columns label conditioned histories, - and $V_{(j,I),i}=w_{ij}$ for normalized output states. Compute $S_V(c)$ from - the normalized mode spectrum. - -For an SVD $V=U\Sigma W^\dagger$, each column pair associated with a retained, -nonzero singular value defines a response mode: the column of $U$ gives the -future-response direction, while the column of $W$ gives a combination of -conditioned histories. With `U = result.left_singular_vectors(c)`, -`s = result.singular_values_full(c)`, and -`W = result.right_singular_vectors(c)`, the full factors satisfy -`V = U @ np.diag(s) @ W.conj().T`. The full factors also contain directions -paired with exact zeros or an unresolved numerical tail. Do not interpret those -directions as resolved memory modes. Singular vectors are also not unique inside -a degenerate singular subspace. - -Hamiltonian `characterize` obtains joint probabilities of the retained outcomes -from the simulated intervention sequence (MCWF or TJM/MPS, per -`representation`). Process-tensor backends obtain the same probabilities from -the trace of each subnormalized contraction, while surrogates estimate them from -their predicted pre-intervention reduced states. - -### Coupling strength and memory - -Stronger Ising coupling $J$ between the probe and the environment typically -increases cross-cut memory. Reuse one `probe_set` when sweeping $J$: - -```{code-cell} ipython3 -j_values = np.linspace(0.0, 2.0, 9) -anchor = mc.characterize( - Hamiltonian.ising(length=length, J=0.0, g=1.0), +```{code-cell} python +num_interventions = 4 +cut = 2 +anchor = characterizer.characterize( + Hamiltonian.ising(length, J=0.0, g=1.0), params, num_interventions=num_interventions, cut=cut, - n_pasts=8, - n_futures=8, - initial_psi=psi0, - rng=np.random.default_rng(42), + preset="quick", + rng=np.random.default_rng(7), ) -entropies = [] -for j in j_values: - result = mc.characterize( - Hamiltonian.ising(length=length, J=float(j), g=1.0), + +print(anchor.summary()) +print("Response matrix shape:", anchor.response_matrix(cut).shape) +``` + +`preset="quick"` selects eight past probes and eight future probes, giving 64 +sequences. This preset sets the probe grid; the preset on `AnalogSimParams` sets +numerical accuracy. For Hamiltonian characterization, `dt` sets the interval +between interventions. `elapsed_time` does not set the full probing horizon. +Here it equals one interval so the simulation parameters have a valid time grid. + +For each past–future pair, YAQS retains the selected measurement outcome and +records the final probe's $(I,X,Y,Z)$ responses. It multiplies each conditional +response by the joint probability of the retained outcomes. The +**response matrix** $V$ places each history in a column and each future's four +response channels in consecutive rows. Its shape here is $(32,8)$, and its +identity rows contain the outcome probabilities. Keeping these probabilities +avoids treating a rare branch as though it occurred on every run. + +## 3. Sweep the coupling with the same probes + +Reuse `anchor` as `probe_set` so every coupling uses the same controls, +measurements, and preparations. Otherwise a change in the sampled probes could +be confused with a change in the environment. + +```{code-cell} python +memories = [anchor] +for coupling in couplings[1:]: + memory = characterizer.characterize( + Hamiltonian.ising(length, J=float(coupling), g=1.0), params, num_interventions=num_interventions, cut=cut, probe_set=anchor, ) - entropies.append(result.entropy(cut)) - -fig, ax = plt.subplots(figsize=(5.5, 3)) -ax.plot(j_values, entropies, "o-") -ax.set_xlabel(r"Ising coupling $J$") -ax.set_ylabel(r"$S_V(c)$") -ax.set_title(r"Environmental memory grows with probe-environment coupling") -fig.tight_layout() + memories.append(memory) + +mode_weights = [] +for memory in memories: + spectrum = memory.singular_values(cut) + mode_weights.append(spectrum**2 / np.sum(spectrum**2)) +entropies = np.array([memory.entropy(cut) for memory in memories]) ``` -### Intervention styles +The singular values $s_k$ describe independent combinations of past settings and +future responses. Their normalized squared weights and entropy are -`characterize` accepts `intervention_style=` (default `"haar"`): +$$ +p_k=\frac{s_k^2}{\sum_j s_j^2},\qquad +S_V=-\sum_k p_k\ln p_k. +$$ -- **`"haar"`** — random unitaries on sequence legs; measure/prepare only at the - causal cut. -- **`"measure_prepare"`** — rank-1 measure–prepare maps on every leg. -- **`"clifford"`** — random single-qubit Clifford gates on legs. +`singular_values(cut)` returns the spectrum retained for this entropy, after +removing a numerical tail with relative squared weight at most $10^{-12}$. +`singular_values_full(cut)` returns every compact-SVD value. The entropy uses +natural logarithms, and `memory.modes(cut)` gives the effective mode number +$R=\exp(S_V)$. One retained mode gives $S_V=0$ and $R=1$. -Pass `probe_set=` from a Hamiltonian run so surrogate or exact-reference -backends evaluate the **same** probe ensemble ({doc}`memory_surrogate`). -Surrogate characterization also requires `initial_rho=`: the site-0 density -matrix after the schedule's initial evolution segment and before its first -intervention. For a surrogate trained against a reference process tensor, use -that tensor's `initial_rho`. +## 4. Read the spectrum and entropy -(reset-delay)= +The spectrum shows how coupling redistributes the response among modes. The +entropy summarizes this spread, allowing us to compare the same probing +experiment across the coupling sweep. -## Memory persistence: conditioned reset delay - -Pass `delay=N`, for any $N\geq0$, to use the conditioned-reset protocol from -Figure 5 of the response-matrix paper. The intervention at the history boundary -applies the selected measurement and prepares $\lvert0\rangle$. YAQS then -inserts $N$ selected-zero reset slots $(\lvert0\rangle,\lvert0\rangle)$ and -applies a second selected-zero measurement before the sampled future -preparation. The environment keeps evolving between these interventions. The -selected history outcome and every selected-zero outcome contribute to the -complete branch probability. - -The two boundary interventions remain separate at `delay=0`. The physical -sequence length is therefore `num_interventions + delay + 1` for every explicit -delay. Omitting `delay` uses the standard one-step causal break -`(selected_history_measurement, sampled_future_preparation)` instead. This keeps -ordinary characterization aligned across Hamiltonian, process-tensor, and -surrogate backends. - -Extra reset time lets the environment decouple from the past before future -controls act, so $S_V(c)$ often decreases at strong probe-environment coupling. -Weaker coupling can show a nonmonotonic profile. The example below uses a -smaller probe grid and shorter sequences than the paper campaign, but it uses -the same conditioned-reset geometry. Reuse the same `probe_set` across the delay -sweep. An explicit `delay` is supported for Hamiltonian characterization only. - -```{code-cell} ipython3 -delay_length = 6 -ham_delay = Hamiltonian.ising(length=delay_length, J=2.0, g=1.0) -params_delay = AnalogSimParams(dt=0.1) -mc_delay = MemoryCharacterizer(show_progress=False) -delay_cut = 4 -delay_k = 6 -anchor_delay = mc_delay.characterize( - ham_delay, - params_delay, - num_interventions=delay_k, - cut=delay_cut, - delay=0, - n_pasts=6, - n_futures=6, - initial_psi=make_zero_psi(delay_length), - rng=np.random.default_rng(999_991), -) -delays = [0, 1, 2, 3] -delay_entropies = [] -for delay in delays: - result = mc_delay.characterize( - ham_delay, - params_delay, - num_interventions=delay_k, - cut=delay_cut, - delay=delay, - probe_set=anchor_delay, - ) - delay_entropies.append(result.entropy(delay_cut)) - -fig, ax = plt.subplots(figsize=(4.5, 3)) -ax.plot(delays, delay_entropies, "s-") -ax.set_xlabel("reset delay at causal cut") -ax.set_ylabel(r"$S_V(c)$") -ax.set_title(r"Strong coupling: memory erodes with longer reset delay") -ax.set_xticks(delays) -fig.tight_layout() +```{code-cell} python +:tags: [hide-input] +import matplotlib.pyplot as plt +from matplotlib.colors import LinearSegmentedColormap, Normalize +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", + "font.serif": ["STIXGeneral"], + "mathtext.fontset": "stix", + "font.size": 10, + "axes.labelsize": 11, + "axes.linewidth": 0.8, + "xtick.direction": "in", + "ytick.direction": "in", + "svg.fonttype": "none", +}) +red_map = LinearSegmentedColormap.from_list("coupling_reds", plt.colormaps["Reds"](np.linspace(0.3, 0.95, 100))) +norm = Normalize(couplings[0], couplings[-1]) +fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.8), layout="constrained") +for coupling, weights in zip(couplings, mode_weights, strict=True): + axes[0].semilogy(np.arange(1, len(weights) + 1), weights, "o-", color=red_map(norm(coupling)), lw=1.3, ms=3) +axes[0].set(xlabel="Mode index", ylabel=r"Retained mode weight $p_k$", ylim=(1e-13, 2), xticks=[1, 2, 4, 6, 8]) +fig.colorbar(plt.cm.ScalarMappable(norm=norm, cmap=red_map), ax=axes[0], label=r"Coupling $J/g$", ticks=[0, 0.5, 1, 1.5], fraction=0.05, pad=0.03) +axes[1].fill_between(couplings, entropies, color=red_map(0.35), alpha=0.3) +axes[1].plot(couplings, entropies, "o-", color=red_map(0.95), lw=2.2, ms=4, markerfacecolor="white") +axes[1].set(xlabel=r"Coupling $J/g$", ylabel=r"Memory entropy $S_V$ (nats)", xlim=(-0.03, 1.53), ylim=(-0.01, 0.34), xticks=[0, 0.5, 1, 1.5]) +for label, ax in zip(("(a)", "(b)"), axes, strict=True): + ax.text(0.02, 1.03, label, transform=ax.transAxes, va="bottom", fontweight="bold") + ax.spines[["top", "right"]].set_visible(False) +plt.show() ``` -## Representation +**Coupling creates additional response modes, with a peak inside this sweep.** +(a) Darker curves show larger $J$. The uncoupled probe has one retained mode; +coupled dynamics distribute weight among additional modes. (b) The entropy +reaches a maximum near $J/g=1.4$, then decreases. Stronger coupling does not +imply a larger entropy for a fixed schedule and probe grid. -`AnalogSimParams` configures the evolution and does not select the memory -backend. `MemoryCharacterizer(representation="auto")` mirrors `Simulator`: -`"vector"` selects MCWF, `"mps"` selects TJM for the **environment** chain. With -`"auto"`, MCWF is used when `hamiltonian.length <= vector_max_qubits` (default -10). +These weights describe the responses accessible through the chosen experiment. +They are not environment populations or Schmidt weights of a mixed state. A +small entropy means that a few modes dominate these responses; it does not prove +that every possible experiment would find the environment memoryless. Changing +the interval, temporal cut, or controls can reveal different memory. The +spectrum alone also does not certify that the memory is quantum rather than +classical. -## Temporal entanglement from a process tensor +## Further experiments -Operational memory ($S_V$) comes from probe responses. **Temporal entanglement** -$S_{PT}(c)$ is computed directly from a process tensor at the same causal cut. -By default, `build_process_tensor` uses direct MPO construction -(`return_type="mpo"`). Pass `return_type="dense"` for exhaustive tomography -(required for `noise_model`): +(reset-delay)= -```{code-cell} ipython3 -k = 2 -cut_pt = 1 -timesteps = [0.1] * (k + 1) +### How long does memory persist under resets? -pt_mpo = mc.build_process_tensor( - ham, - params, - timesteps=timesteps, -) -pt_dense = mc.build_process_tensor( - ham, - params, - timesteps=timesteps, - return_type="dense", -) +A causal break interrupts the probe once. Repeated resets ask whether the +selected histories remain distinguishable after a longer interruption. With +`delay=N`, YAQS measures the selected history outcome and prepares $|0\rangle$, +inserts $N$ selected-zero measure–prepare resets, then measures zero once more +before the sampled future preparation. The environment evolves between all these +interventions. -s_mpo = pt_mpo.compute_temporal_entropy(cut_pt) -s_dense = pt_dense.compute_temporal_entropy(cut_pt) -print( - f"S_PT(c={cut_pt}): mpo={s_mpo['entropy']:.4f}, " - f"dense={s_dense['entropy']:.4f}, schmidt_rank={s_mpo['schmidt_rank']}" -) +```{code-cell} python +hamiltonian = Hamiltonian.ising(length, J=1.0, g=1.0) +delays = np.arange(7) +delay_memories = [] +for delay in delays: + delay_memories.append(characterizer.characterize( + hamiltonian, + params, + num_interventions=num_interventions, + cut=cut, + delay=int(delay), + probe_set=anchor, + )) +delay_entropies = [memory.entropy(cut) for memory in delay_memories] +``` -# The same exact process tensor also supports operational memory via characterize: -pt_result = mc.characterize( - pt_mpo, - cut=cut_pt, - num_interventions=k, - n_pasts=6, - n_futures=6, - rng=np.random.default_rng(7), -) -print(f"S_V(c={cut_pt}) from process-tensor probes: {pt_result.entropy(cut_pt):.4f}") +Every integer delay, including zero, uses separate boundary interventions. The +sequence has `num_interventions + delay + 1` interventions and one more +evolution interval. Omitting `delay` uses the standard single causal break, so +the ordinary result and the `delay=0` result have different schedules. + +```{code-cell} python +:tags: [hide-input] +fig, ax = plt.subplots(figsize=(4.5, 2.6), layout="constrained") +ax.plot(delays, delay_entropies, "o-", color="#225c80", lw=2, ms=5, markerfacecolor="white") +ax.fill_between(delays, delay_entropies, color="#225c80", alpha=0.12) +ax.set(xlabel="Selected-zero reset slots", ylabel=r"Memory entropy $S_V$ (nats)", xticks=delays, ylim=(0, None)) +ax.spines[["top", "right"]].set_visible(False) +plt.show() +``` + +**The conditioned memory varies nonmonotonically with reset delay.** The finite +spin environment continues to evolve and can return information to the probe. +These resets retain particular outcomes rather than averaging over every +measurement result. Their joint probability contributes to $V$, so this is a +conditioned persistence experiment. It does not establish an all-outcome memory +length. Explicit `delay` is supported for Hamiltonian characterization. + +### What changes when we add dephasing? + +The Hamiltonian `characterize` path does not accept a `NoiseModel`. To include +Markovian noise, first reconstruct a **dense process tensor**, which records the +response to interventions, then pass that tensor to `characterize`. We use two +spins and one causal break to keep this exhaustive reconstruction small. Two +evolution intervals of 0.5 surround the break; the probe and its one-spin +environment retain $J=g=1$. + +```{code-cell} python +from mqt.yaqs import NoiseModel + +short_hamiltonian = Hamiltonian.ising(2, J=1.0, g=1.0) +noise_params = AnalogSimParams(elapsed_time=0.5, dt=0.025, preset="fast", random_seed=7) +dephasing_rates = [0.0, 1.0, 4.0] +process_tensors = [] +noise_memories = [] +short_anchor = None +for rate in dephasing_rates: + noise = None if rate == 0 else NoiseModel([{"name": "pauli_z", "sites": [0], "strength": rate}]) + process = characterizer.build_process_tensor( + short_hamiltonian, + noise_params, + timesteps=[0.5, 0.5], + return_type="dense", + noise_model=noise, + num_trajectories=512, + ) + memory = characterizer.characterize( + process, + cut=1, + preset="quick", + probe_set=short_anchor, + rng=np.random.default_rng(7), + ) + short_anchor = memory if short_anchor is None else short_anchor + process_tensors.append(process) + noise_memories.append(memory) + +for rate, memory in zip(dephasing_rates, noise_memories, strict=True): + print(f"Dephasing rate {rate:g}: {memory.summary()}") ``` -Dense and default direct MPO construction agree on $S_{PT}$ for small $k$. The -supported direct path is uncapped: at intervention leg $k$, it can retain up to -$16^k$ histories and construct the same number of rank-one terms. Use it only -for short horizons. `compress_every` limits an accumulation batch; it does not -limit the number of histories. Dense tomography has the same $16^k$ sequence -count and is required when you use `noise_model`. +Here `strength=rate` is a Lindblad rate, with jump operator +$L=\sqrt{\gamma}\,Z_0$ and dissipator $\gamma(Z_0\rho Z_0-\rho)$. `timesteps` +defines the two evolution intervals, while `noise_params.dt` sets the +integration step within them. Each of the 16 tomography sequences averages 512 +trajectories for nonzero noise. The noiseless reconstruction uses one. + +```{code-cell} python +:tags: [hide-input] +fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.6), layout="constrained") +noise_colors = ["#225c80", "#b78730", "#bb563b"] +for rate, memory, color in zip(dephasing_rates, noise_memories, noise_colors, strict=True): + spectrum = memory.singular_values(1) + weights = spectrum**2 / np.sum(spectrum**2) + axes[0].semilogy(np.arange(1, len(weights) + 1), weights, "o-", color=color, ms=4, lw=1.6, label=rf"$\gamma/g={rate:g}$") +axes[0].set(xlabel="Mode index", ylabel=r"Retained mode weight $p_k$", xticks=[1, 2, 3, 4], ylim=(1e-8, 2)) +axes[0].legend(frameon=False, fontsize=9, loc="lower left") +axes[1].bar(np.arange(3), [memory.entropy(1) for memory in noise_memories], color=noise_colors, width=0.6) +axes[1].set(xlabel=r"Dephasing rate $\gamma/g$", ylabel=r"Memory entropy $S_V$ (nats)", xticks=np.arange(3), xticklabels=["0", "1", "4"]) +for label, ax in zip(("(a)", "(b)"), axes, strict=True): + ax.text(0.02, 1.03, label, transform=ax.transAxes, va="bottom", fontweight="bold") + ax.spines[["top", "right"]].set_visible(False) +plt.show() +``` + +**Dephasing reduces the weight outside the leading response mode.** (a) The +leading mode gains relative weight as the dephasing rate increases. (b) The +sampled entropy falls. These values describe the two-spin, one-break schedule +and should not be compared directly with the earlier four-intervention sweep. +Small spectral weights can reflect trajectory sampling error; they do not all +establish resolved physical modes. Increase the trajectory count and refine the +integration step before interpreting the smallest weights. + +Coupling, resets, and added dephasing answer different questions about the same +physical issue: which traces of earlier probe choices can affect later +responses? Keep the probes and schedule fixed when comparing models, and check +whether the observed spectrum persists as numerical and sampling accuracy +improve. + +## Further options + +### Probe coverage and representations + +`preset="balanced"` uses a $32\times32$ grid and `"accurate"` uses +$128\times128$. Set `n_pasts` and `n_futures` to choose counts explicitly. +Larger grids test more controls; they are not extra trajectories of the same +experiment. Check probe coverage as well as evolution accuracy. + +Use `intervention_style="clifford"` for random single-qubit Clifford controls, +or `"measure_prepare"` for rank-one measurement and preparation on every leg. +The default `"haar"` uses random unitaries away from the cut. The choice changes +what memory the experiment can resolve. + +Pass `cuts=[...]` or `cuts="all"` to compare temporal cuts. Each cut needs its +own probe geometry, so `probe_set` reuse is restricted to a single cut. +`representation="auto"` on `MemoryCharacterizer` selects vectors up to ten +qubits and MPS above that size. Set `"vector"` or `"mps"` explicitly when +needed. `initial_psi` replaces the default all-zero state for Hamiltonian +characterization. See {class}`~mqt.yaqs.MemoryCharacterizer` for execution and +accuracy options. + +### Inspecting response modes + +Use `memory.response_matrix(cut)` to inspect $V$. The full compact-SVD factors +from `left_singular_vectors`, `singular_values_full`, and +`right_singular_vectors` satisfy $V=U\operatorname{diag}(s)W^\dagger$. The +columns of $U$ describe future responses, while the columns of $W$ combine +histories. Directions paired with an unresolved tail are not resolved modes, and +vectors inside a degenerate singular subspace are not unique. + +### Process tensors and temporal entanglement + +`build_process_tensor` defaults to direct MPO construction for noiseless models. +Use `return_type="dense"` for added noise, as above. Both constructions grow +with $16^k$ intervention sequences or histories, so reserve them for short +horizons. Direct construction retains all branches by default; `compress_every` +controls an accumulation batch, not the total number of histories. + +A process tensor also provides `compute_temporal_entropy(cut)`, `qmi`, and +`cmi`. These describe the multi-time process, while $S_V$ describes the sampled +probe responses. They are distinct quantities. Reuse the noiseless short-horizon +tensor to compute its temporal operator-Schmidt entropy: + +```{code-cell} python +temporal = process_tensors[0].compute_temporal_entropy(1) +print(f"Temporal entropy S_PT: {temporal['entropy']:.4f}") +print(f"Response entropy S_V: {noise_memories[0].entropy(1):.4f}") +``` -`MPOProcessTensor.compute_temporal_entropy()`, `MPOProcessTensor.qmi()`, and -`MPOProcessTensor.cmi()` currently convert the complete MPO to a dense matrix. -The matrix alone uses $64\,16^k$ bytes for $k$ intervention legs, before -analysis workspace. On a typical workstation, restrict these calculations to -about five legs. The matrix uses 64 MiB at five legs and 1 GiB at six legs. +Both entropies use natural logarithms. Their values differ because they describe +different objects. The MPO implementations of these diagnostics currently +densify the tensor. Dense storage alone takes 64 MiB at five intervention legs +and 1 GiB at six, before analysis workspace. Operational probing of an MPO +process tensor does not require this conversion. ```{warning} -Passing a finite `max_bond_dim` enables experimental direct-MPO truncation and -emits a `RuntimeWarning`. This uncontrolled approximation can change the process -tensor and does not preserve positivity or causal normalization. Do not use a -capped result as a stable scientific reference. +A finite `max_bond_dim` in direct process-tensor construction enables an +experimental approximation that can violate positivity and causal normalization. +Keep the supported default `None` for scientific references. Noisy tomography +also has finite-sample error; validate reconstructed responses before using them +as a reference. ``` -Operational characterization requires each contracted branch to be Hermitian and -positive semidefinite, with trace in $[0,1]$. Response assembly also checks that -each normalized qubit response lies in the Bloch ball. QMI and CMI always -normalize the process tensor and validate positive semidefiniteness and causal -normalization. Remove an experimental finite cap by setting `max_bond_dim=None`, -or use a sufficiently accurate dense reconstruction when you need $S_V$ from a -process tensor. `characterize(pt, ...)` uses native MPO -`evaluate_probes_with_weights` without densifying the V-matrix path. - -## Related topics - -- {doc}`quickstart` — minimal characterize and surrogate predict snippets -- {doc}`memory_surrogate` — train a surrogate, predict dynamics, validate - against exact references -- API reference: :class:`~mqt.yaqs.memory_characterizer.MemoryCharacterizer` +For predicting dynamics under new controls with a trained model, see +{doc}`memory_surrogate`. Surrogate characterization requires `initial_rho`, the +site-0 state after initial evolution and before the first intervention. Use the +reference process tensor's `initial_rho` when the surrogate was trained against +that tensor, and reuse the same `probe_set` for a direct comparison. diff --git a/docs/examples/digital_twin.md b/docs/examples/digital_twin.md index 6ef752796..99d5278ca 100644 --- a/docs/examples/digital_twin.md +++ b/docs/examples/digital_twin.md @@ -2,276 +2,319 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true - execution_timeout: 900 + execution_timeout: 300 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Building a Digital Twin -Build a **digital twin** of an open quantum system: learn unknown Lindblad jump -rates from observable time series via simulator forward modeling and CMA-ES -(analytical optimization), validate the fit on the measured traces, then deploy -the learned model in {class}`~mqt.yaqs.Simulator` to predict **held-out** -observables. +Noise changes how excitations move through a quantum system. Measurements can +help us estimate that noise and build a model of the observed dynamics. Here we +learn relaxation and dephasing rates from measurements at the ends of a +four-spin chain, then use the fitted model to predict transport through its +unmeasured interior. -The entry point is {class}`~mqt.yaqs.NoiseCharacterizer`. +This extends {doc}`quickstart` with data preparation, parameter bounds, and +validation. We generate synthetic observations so the underlying rates are +known, but pass only the observed traces to the fitter. The example uses the +standard YAQS installation and Matplotlib for plotting. Run the cells in order +in a notebook; for a script, use the entry-point guard in +{doc}`simulator_initialization`. -```{note} -A machine-learning pipeline with the same I/O (reference trajectories in, fitted -``NoiseModel`` out) is planned for a future release. -``` +## 1. Set up excitation transport + +The XY Hamiltonian exchanges excitations between neighboring spins, + +$$ +H=-\frac{1}{2}\sum_{i=0}^{2}(X_iX_{i+1}+Y_iY_{i+1}). +$$ + +We start with one excitation at site 0. As in {doc}`analog_simulation`, the +hopping amplitude is one and $\hbar=1$. Measuring $Z_i$ gives the occupation +through $\langle n_i\rangle=(1-\langle Z_i\rangle)/2$. + +```{code-cell} python +import numpy as np + +from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseCharacterizer, NoiseModel, Observable, Simulator, State -```{note} -Rates are not always uniquely identifiable from a sparse observable set. Judge a -fit by **trajectory overlap** first; rate bars are secondary validation. +length = 4 +state = State(length, initial="basis", basis_string="1000", representation="density_matrix") +hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) +observables = [Observable("z", site) for site in range(length)] +params = AnalogSimParams(observables=observables, elapsed_time=8.0, dt=0.1, preset="fast") +simulator = Simulator(show_progress=False) ``` -```{note} -**Forward backends:** `representation="auto"` (default) prefers deterministic -Lindblad on small chains, then MCWF (`"vector"`), then TJM (`"mps"`). See -{doc}`representation_comparison` for cross-backend validation. +The density-matrix state gives deterministic Lindblad dynamics without +trajectory sampling error. We record all four sites to validate predictions +later; only the endpoints will enter the fit. The documentation suppresses +progress bars with `show_progress=False`; omit this argument to see progress. + +## 2. See how noise changes the dynamics + +A local relaxation channel at site 3 removes excitations after they reach the +far end of the chain. A dephasing channel at site 2 leaves the total excitation +number unchanged by itself, but changes the interference that drives transport. +The reference rates are $\gamma_{\mathrm{loss}}=0.35$ and $\gamma_\phi=0.12$. + +```{code-cell} python +def transport_noise(scale): + """Scale the reference relaxation and dephasing rates together.""" + return NoiseModel([ + {"name": "lowering", "sites": [3], "strength": 0.35 * scale}, + {"name": "pauli_z", "sites": [2], "strength": 0.12 * scale}, + ]) + + +noise_scales = [0.0, 0.5, 1.0, 3.0] +reference_runs = {} +occupations = {} +for scale in noise_scales: + noise = None if scale == 0 else transport_noise(scale) + result = simulator.run(state, hamiltonian, params, noise) + reference_runs[scale] = result + occupations[scale] = (1 - np.asarray(result.expectation_values)) / 2 + +times = reference_runs[1.0].times ``` -## 1. Minimal fit +`strength` is a Lindblad rate in inverse time. The jump operators are +$L_{\mathrm{loss}}=\sqrt{\gamma_{\mathrm{loss}}}\,|0\rangle\langle1|_3$ and +$L_\phi=\sqrt{\gamma_\phi}\,Z_2$. This convention gives the dephasing term +$\gamma_\phi(Z_2\rho Z_2-\rho)$. The dimensionless `scale` multiplies both +rates; it is not a per-gate error probability. -Three-site transverse-field Ising chain with homogeneous Pauli noise. Pass -`reference_model=` to simulate target trajectories internally (benchmark -shortcut); for lab data use `ref_expectations=` instead (section 3). +The four heatmaps share their axes and a square-root color normalization, which +keeps weak occupation visible while retaining the full range from zero to one. -```{code-cell} ipython3 +```{code-cell} python +:tags: [hide-input] import matplotlib.pyplot as plt -import numpy as np +from matplotlib.colors import PowerNorm +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", + "font.serif": ["STIXGeneral"], + "mathtext.fontset": "stix", + "font.size": 10, + "axes.labelsize": 11, + "axes.linewidth": 0.8, + "xtick.direction": "in", + "ytick.direction": "in", + "svg.fonttype": "none", +}) +occupation_norm = PowerNorm(gamma=0.5, vmin=0, vmax=1) +fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.4), sharex=True, sharey=True, layout="constrained") +titles = ["(a) No noise", "(b) Half the reference rates", "(c) Reference rates", "(d) Three times the reference rates"] +for ax, scale, title in zip(axes.flat, noise_scales, titles, strict=True): + image = ax.pcolormesh(times, np.arange(length), occupations[scale], shading="auto", cmap="cividis", norm=occupation_norm, rasterized=True) + ax.set(title=title, yticks=np.arange(length), xlim=(0, 8)) +for ax in axes[-1]: + ax.set_xlabel(r"Time $t$") +for ax in axes[:, 0]: + ax.set_ylabel(r"Site $i$") +fig.colorbar(image, ax=axes, label=r"Occupation $\langle n_i\rangle$", ticks=[0, 0.25, 0.5, 1], shrink=0.92) +plt.show() +``` -from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseCharacterizer, NoiseModel, Observable, Simulator, State +**Noise changes transport and weakens the returning excitation.** Without noise, +the excitation reflects through the chain while its total population stays one. +Relaxation removes population, and dephasing changes its spatial distribution. +Both rates change together in this comparison, so the panels show their combined +effect. We will fit the reference-rate case and use the other panels only to +illustrate the physical changes. -n_sites = 3 -j_coupling = 1.0 -transverse_field = 2.0 -gamma_true = 0.08 -gamma_init = 0.35 -cma_seed = 42 -sites = list(range(n_sites)) - -hamiltonian = Hamiltonian.ising(n_sites, J=j_coupling, g=transverse_field) -init_state = State(n_sites, initial="zeros") - -fitting_observables = [ - Observable("y", 0), - Observable("z", 0), - Observable("y", 1), -] -prediction_observables = [ - Observable("x", 0), - Observable("x", 1), - Observable("x", 2), - Observable("z", 2), -] - -sim_params = AnalogSimParams( - observables=fitting_observables, - elapsed_time=0.8, - dt=0.1, - order=1, - sample_timesteps=True, -) +## 3. Select the observations and candidate channels -reference_model = NoiseModel( - [{"name": "pauli_x", "sites": [s], "strength": gamma_true} for s in sites] - + [{"name": "pauli_y", "sites": [s], "strength": gamma_true} for s in sites] - + [{"name": "pauli_z", "sites": [s], "strength": gamma_true} for s in sites] -) +Assume that only the endpoints can be measured. Select rows 0 and 3 of the +synthetic $Z$ traces, keeping their time samples unchanged. -init_guess = NoiseModel( - [{"name": "pauli_x", "sites": [s], "strength": gamma_init} for s in sites] - + [{"name": "pauli_y", "sites": [s], "strength": gamma_init} for s in sites] - + [{"name": "pauli_z", "sites": [s], "strength": gamma_init} for s in sites] -) +```{code-cell} python +fitting_sites = [0, 3] +fitting_observables = [observables[site] for site in fitting_sites] +reference_z = np.asarray(reference_runs[1.0].expectation_values) +measured_z = reference_z[fitting_sites] -rate_bounds_low = np.zeros(len(init_guess.processes)) -rate_bounds_high = np.full(len(init_guess.processes), 0.5) -pauli_labels = ["X", "Y", "Z"] +print("Observation shape:", measured_z.shape) +``` -nc = NoiseCharacterizer(show_progress=False) -result = nc.characterize( - hamiltonian, - sim_params, - init_state=init_state, - init_guess=init_guess, - observables=fitting_observables, - reference_model=reference_model, - x_low=rate_bounds_low, - x_up=rate_bounds_high, - sigma0=0.05, - popsize=8, - max_iter=40, - seed=cma_seed, -) +`ref_expectations` must have shape `(n_observables, n_times)`. Here it is +`(2, 81)`: rows follow `fitting_observables`, and columns follow `params.times`, +including $t=0$. We fit $Z$ expectations, not occupations; convert measured +occupations with `measured_z = 1 - 2 * measured_occupations` when necessary. +Data from another time grid must first be aligned with the simulation grid. -gamma_learned = np.array([ - result.best_parameters[0:n_sites].mean(), - result.best_parameters[n_sites : 2 * n_sites].mean(), - result.best_parameters[2 * n_sites : 3 * n_sites].mean(), -]) -times = result.times -print(f"√J: {result.sqrt_loss_before():.3f} → {result.sqrt_loss_after():.2e}") -print(f"fitting trajectory RMSE: {result.trajectory_rmse():.2e}") -``` +The candidate model specifies which channels exist and where they act. The +optimizer will change their strengths only. We start both rates at 0.2 and allow +each to range from zero to one. -## 2. Validate fitted dynamics and rates - -```{code-cell} ipython3 -gamma_reference = np.full(len(pauli_labels), gamma_true) -ref_traj = result.ref_traj -fit_traj = result.fit_traj - -fig, axes = plt.subplots(1, 3, figsize=(9, 2.8), gridspec_kw={"width_ratios": [1.1, 1.0, 1.0]}) - -x_pos = np.arange(len(pauli_labels)) -bar_width = 0.35 -axes[0].bar(x_pos - bar_width / 2, gamma_reference, bar_width, label=r"$\gamma_{\mathrm{true}}$", color="0.35") -axes[0].bar(x_pos + bar_width / 2, gamma_learned, bar_width, label="learned twin", color="C0") -axes[0].set_xticks(x_pos, pauli_labels) -axes[0].set_ylabel(r"$\gamma$") -axes[0].set_title("Learned rates vs. hidden truth") -axes[0].legend(loc="upper right", fontsize=8) - -fit_panels = [(0, r"$\langle Y_0\rangle$"), (1, r"$\langle Z_0\rangle$")] -for ax, (obs_idx, ylabel) in zip(axes[1:], fit_panels, strict=True): - ax.plot(times, fit_traj[obs_idx], color="C0", lw=2.5, label="twin", zorder=1) - ax.plot(times, ref_traj[obs_idx], color="0.2", ls=":", lw=2.5, label="experiment", zorder=2) - ax.set_xlabel("time") - ax.set_ylabel(ylabel) - ax.set_ylim(-1.05, 1.05) - panel_rmse = float(np.sqrt(np.mean((fit_traj[obs_idx] - ref_traj[obs_idx]) ** 2))) - ax.text(0.03, 0.06, rf"RMSE={panel_rmse:.1e}", transform=ax.transAxes, fontsize=8) - ax.legend(loc="upper right", fontsize=8) - -fig.suptitle("Twin reproduces the experimental fitting observables", y=1.05, fontsize=11) -fig.tight_layout() +```{code-cell} python +initial_guess = NoiseModel([ + {"name": "lowering", "sites": [3], "strength": 0.2}, + {"name": "pauli_z", "sites": [2], "strength": 0.2}, +]) +lower_bounds = np.zeros(2) +upper_bounds = np.ones(2) ``` -## 3. Experimental data +Bounds and fitted parameters follow the order of `initial_guess.processes`: +relaxation first, dephasing second. These bounds restrict the search; they are +not uncertainty intervals. The fit assumes the Hamiltonian, initial state, +channel types, and channel locations are known. It does not discover an +arbitrary noise model from the observations. -When trajectories come from the lab (or an external simulator), pass them as -`ref_expectations` with shape `(n_obs, n_times)` matching `observables` and -`sim_params.times`. Below we reuse the reference trajectories from section 1 as -a stand-in for measured data. +## 4. Fit the rates -```{code-cell} ipython3 -experimental_data = np.asarray(result.ref_traj, dtype=float) +`NoiseCharacterizer` repeatedly simulates candidate models and minimizes the +mean-squared difference from the supplied traces. Its default backend selection +uses deterministic Lindblad evolution for this four-spin problem. -lab_result = NoiseCharacterizer(show_progress=False).characterize( +```{code-cell} python +characterizer = NoiseCharacterizer(show_progress=False) +fit = characterizer.characterize( hamiltonian, - sim_params, - init_state=init_state, - init_guess=init_guess, + params, + init_state=state, + init_guess=initial_guess, observables=fitting_observables, - ref_expectations=experimental_data, - x_low=rate_bounds_low, - x_up=rate_bounds_high, - sigma0=0.05, - popsize=8, + ref_expectations=measured_z, + x_low=lower_bounds, + x_up=upper_bounds, max_iter=40, - seed=cma_seed, + seed=7, ) -print(f"lab-data fit RMSE: {lab_result.trajectory_rmse():.2e}") + +print(f"Endpoint Z-trace RMSE: {fit.sqrt_loss_before():.4f} → {fit.trajectory_rmse():.2e}") +for name, rate in zip(("Relaxation", "Dephasing"), fit.best_parameters, strict=True): + print(f"{name} rate: {rate:.4f}") ``` -## 4. Predict held-out observables with the twin +With two free parameters, YAQS uses the derivative-free CMA-ES optimizer. +`max_iter=40` limits its generations, and `seed` fixes the optimizer's random +search. This seed is separate from `AnalogSimParams.random_seed`, which controls +stochastic simulation. Parallel execution remains enabled by default. -Plug `result.optimal_model` into {class}`~mqt.yaqs.Simulator` and compare to the -hidden reference on observables **not** used during fitting. +For observations $z_{o,t}$, the fitted objective is -```{code-cell} ipython3 -pred_params = AnalogSimParams( - observables=prediction_observables, - elapsed_time=sim_params.elapsed_time, - dt=sim_params.dt, - order=sim_params.order, - sample_timesteps=True, -) -simulator = Simulator(show_progress=False) +$$ +J=\frac{1}{N_{\mathrm{obs}}N_t}\sum_{o,t} +\left(z_{o,t}^{\mathrm{model}}-z_{o,t}^{\mathrm{data}}\right)^2. +$$ -twin_result = simulator.run(init_state, hamiltonian, pred_params, result.optimal_model) -truth_result = simulator.run(init_state, hamiltonian, pred_params, reference_model) -twin_traj = np.asarray(twin_result.expectation_values, dtype=float) -truth_traj = np.asarray(truth_result.expectation_values, dtype=float) - -fig, axes = plt.subplots(1, 2, figsize=(7, 2.8)) -holdout_panels = [(0, r"$\langle X_0\rangle$"), (3, r"$\langle Z_2\rangle$")] -for ax, (obs_idx, ylabel) in zip(axes, holdout_panels, strict=True): - ax.plot(times, twin_traj[obs_idx], color="C0", lw=2.5, label="twin", zorder=1) - ax.plot(times, truth_traj[obs_idx], color="0.2", ls=":", lw=2.5, label="reference", zorder=2) - ax.set_xlabel("time") - ax.set_ylabel(ylabel) - ax.set_ylim(-1.05, 1.05) - ax.legend(loc="upper right", fontsize=8) - -fig.suptitle("Twin predicts observables outside the fitting set", y=1.05, fontsize=11) -fig.tight_layout() -``` +`sqrt_loss_before()` reports the initial model's RMSE. `trajectory_rmse()` +reports the mismatch of the final fitted traces. `best_parameters` gives the +rates in process order, and `optimal_model` is the fitted `NoiseModel` ready for +simulation. The known synthetic rates let us check recovery, but a low training +error alone does not establish unique parameters. -## 5. Stochastic experimental data (MCWF) +## 5. Predict the unmeasured interior -The same workflow works with trajectory-averaged MCWF data. Increase `num_traj` -until observables stabilize; the objective becomes stochastic. +Rerun the fitted model with all four observables. Sites 1 and 2 were withheld +from the optimization, so they test predictions beyond the fitted traces. -```{code-cell} ipython3 -mcwf_sim_params = AnalogSimParams( - observables=fitting_observables, - elapsed_time=0.8, - dt=0.1, - order=1, - num_traj=32, - sample_timesteps=True, -) +```{code-cell} python +reconstructed = simulator.run(state, hamiltonian, params, fit.optimal_model) +fitted_z = np.asarray(reconstructed.expectation_values) +fitted_occupation = (1 - fitted_z) / 2 +heldout_sites = [1, 2] +heldout_rmse = np.sqrt(np.mean((fitted_z[heldout_sites] - reference_z[heldout_sites]) ** 2)) -mcwf_result = NoiseCharacterizer(show_progress=False, representation="vector").characterize( - hamiltonian, - mcwf_sim_params, - init_state=init_state, - init_guess=init_guess, - observables=fitting_observables, - reference_model=reference_model, - x_low=rate_bounds_low, - x_up=rate_bounds_high, - sigma0=0.05, - popsize=8, - max_iter=20, - seed=cma_seed, -) - -fig, ax = plt.subplots(figsize=(4.5, 2.8)) -obs_idx = 1 -ax.plot(times, mcwf_result.fit_traj[obs_idx], color="C0", lw=2.5, label="MCWF twin", zorder=1) -ax.plot(times, mcwf_result.ref_traj[obs_idx], color="0.2", ls=":", lw=2.5, label="experiment", zorder=2) -ax.set_xlabel("time") -ax.set_ylabel(r"$\langle Z_0\rangle$") -ax.set_ylim(-1.05, 1.05) -ax.legend(loc="upper right", fontsize=8) -ax.set_title(f"MCWF fit: √J → {mcwf_result.sqrt_loss_after():.2e}") -fig.tight_layout() +print(f"Withheld interior Z-trace RMSE: {heldout_rmse:.2e}") ``` -## Workflow summary - -| Step | Action | -| ---- | ------------------------------------------------------------------- | -| 1 | Collect experimental trajectories on a fitting observable set | -| 2 | `NoiseCharacterizer.characterize(..., ref_expectations=...)` | -| 3 | Compare learned rates and fitted-observable dynamics to reference | -| 4 | `Simulator.run` with `result.optimal_model` on held-out observables | - -## See also +Compare the full dynamics on the same color scale, then inspect the withheld +sites as time traces. Reference markers are spaced out for readability; all 81 +samples enter the error calculation. + +```{code-cell} python +:tags: [hide-input] +fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.6), layout="constrained") +for ax, dynamics, title in zip( + axes[0], + (occupations[1.0], fitted_occupation), + ("(a) Synthetic reference", "(b) Fitted noise model"), + strict=True, +): + image = ax.pcolormesh(times, np.arange(length), dynamics, shading="auto", cmap="cividis", norm=occupation_norm, rasterized=True) + ax.set(title=title, xlabel=r"Time $t$", ylabel=r"Site $i$", yticks=np.arange(length), xlim=(0, 8)) +fig.colorbar(image, ax=list(axes[0]), label=r"Occupation $\langle n_i\rangle$", ticks=[0, 0.5, 1]) +for ax, site, label in zip(axes[1], heldout_sites, ("(c)", "(d)"), strict=True): + ax.plot(times, fitted_occupation[site], color="#225c80", lw=1.8, label="Fitted prediction") + ax.plot(times[::4], occupations[1.0][site, ::4], "o", color="#bb563b", ms=3.5, markerfacecolor="white", label="Withheld reference") + ax.set(title=f"{label} Withheld site {site}", xlabel=r"Time $t$", ylabel=rf"Occupation $\langle n_{site}\rangle$", xlim=(0, 8), ylim=(0, 1)) + ax.spines[["top", "right"]].set_visible(False) +axes[1, 0].legend(frameon=False, fontsize=9, loc="upper right") +plt.show() +``` -- {doc}`representation_comparison` — Lindblad vs MCWF vs TJM on the same - benchmark -- {doc}`analog_simulation` — open-system simulation overview -- {doc}`characterization` — non-Markovian **memory** characterization (the - memory twin submodule) +**Endpoint observations recover the transport through the interior in this +model.** The fitted heatmap reproduces the reference, including both withheld +sites. This supports the fitted model for the specified Hamiltonian, initial +state, and observation window. It does not certify the assumed channels or +establish accuracy for other preparations, controls, or longer times. In +experimental work, reserve independent measurements for this validation step. + +## Using measured data + +Replace `measured_z` with your measured expectation array and keep the same +observable and time ordering. Supply exactly one of `ref_expectations` and +`reference_model`. The latter generates reference traces internally and is a +shortcut for synthetic benchmarks; it is not needed when measurements are +already available. + +The example contains no measurement noise. Finite-shot data add uncertainty, and +calibration drift or an incorrect Hamiltonian can also affect the fit. The +current objective weights every observable and time sample equally; it does not +accept per-sample uncertainty weights or return confidence intervals for the +rates. Check residuals against measurement uncertainty and test withheld data +before interpreting small differences between fitted parameters. + +Sparse observations can leave several rate combinations indistinguishable. Use +more times, observables, or preparations to test identifiability. A successful +optimization shows that a candidate model fits the chosen data; it does not +prove that this model is unique or that the environment has no memory. For +memory-sensitive probing, see {doc}`characterization`. + +## Further options + +### Forward models and sampling + +`NoiseCharacterizer(representation="auto")` uses density matrices up to eight +qubits, vectors up to ten, and MPS above that size by default. Choose +`"density_matrix"`, `"vector"`, or `"mps"` explicitly when needed. See +{doc}`representation_comparison` for the numerical trade-offs. + +Vector and MPS fits use trajectory-averaged MCWF and TJM simulations. +`sim_params.num_traj` controls their sampling budget; increasing it reduces +sampling error at greater cost. Refine the time step and numerical tolerances as +well as the trajectory count. A fixed `random_seed` makes the forward runs +repeatable but does not remove their sampling error. Recheck the fitted model +with more trajectories and independent seeds before drawing conclusions. For a +small system, deterministic fitting can also use stochastic or measured +reference data without making the candidate simulations stochastic. + +### Optimizer controls and results + +`sigma0` sets the initial CMA-ES search scale, and `popsize` sets the number of +candidates per generation. A larger search budget or several starting points can +help assess sensitivity to initialization. When there is only one free parameter +with finite bounds, YAQS uses a bounded scalar search; `max_iter` then limits +search evaluations rather than CMA-ES generations. Initial-model and final +fitted-trajectory evaluations are outside either limit. + +`fit.ref_traj`, `fit.fit_traj`, and `fit.times` retain the fitted-observable +comparison. `fit.loss_history` stores candidate losses; it excludes the initial +model's separately evaluated baseline. `fit.best_loss` is the best search +objective, while `fit.sqrt_loss_after()` gives its square root. On stochastic +backends, the final rerun can differ from the best sampled objective. Inspect +the traces as well as the optimizer's reported loss. + +See {class}`~mqt.yaqs.NoiseCharacterizer` for the full interface and +{doc}`realistic_noise_models` for supported one-site and two-site jump +processes. diff --git a/docs/examples/memory_surrogate.md b/docs/examples/memory_surrogate.md index daa5ff9e2..6089ebbb0 100644 --- a/docs/examples/memory_surrogate.md +++ b/docs/examples/memory_surrogate.md @@ -2,308 +2,348 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true execution_timeout: 900 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] +# Predicting Non-Markovian Dynamics + +A control pulse changes a quantum system and its later interaction with the +environment. Predicting that response usually requires evolving the joint system +and environment again for each control sequence. A surrogate learns from +simulated sequences so that we can query new controls without repeating that +evolution. + +Here we train on random controls, then predict how a chosen rotation changes the +final coherence of a probe qubit. We extend {doc}`quickstart` by comparing two +environment couplings and checking each prediction against direct Hamiltonian +evolution. Two qubits keep the reference calculation small; this example teaches +the workflow rather than demonstrating a speed advantage. + +```{note} +**Experimental feature.** Surrogate modeling is not yet supported by a published +YAQS paper. Validate predictions for your controls and time horizon against +reference simulations or measurements. This example uses two interventions; it +does not establish reliable prediction for long control protocols. ``` -# Memory Surrogate Training and Prediction +Install the PyTorch extra with `uv pip install "mqt.yaqs[torch]"`. The example +also uses Matplotlib. Run the cells in order in a notebook; for a script, use +the entry-point guard in {doc}`simulator_initialization`. -For control sequences beyond what you can simulate exhaustively, train a -**causal Transformer surrogate** on Hamiltonian rollouts of the open system. -{meth}`~mqt.yaqs.memory_characterizer.MemoryCharacterizer.predict` returns the -**reduced density matrix of the probe qubit** after a control sequence. +## 1. Choose the system and control times -Surrogate training requires PyTorch (`uv pip install mqt.yaqs[torch]`). Over -**short temporal horizons** (few intervention steps), compare surrogate rollouts -to Hamiltonian training targets and to exact **dense** or -**MPO process tensors** built with -{meth}`~mqt.yaqs.memory_characterizer.MemoryCharacterizer.build_process_tensor`. -Environmental memory probing (`characterize`) is covered in -{doc}`characterization`. +Site 0 is the probe, and site 1 is an unobserved environment qubit. Their +Hamiltonian is -```{warning} -Exact references scale exponentially with sequence length. Use them only over -**few intervention steps** — short probes in time, not long open-system runs. -``` +$$ +H=-JZ_0Z_1-g(X_0+X_1), \qquad g=0.5. +$$ -## Setup +The environment starts in $|0\rangle$. During training we vary the probe +preparation and apply random single-qubit rotations to the probe. The joint +state evolves between rotations, so the environment can retain information about +earlier controls. -```{code-cell} ipython3 -import matplotlib.pyplot as plt +```{code-cell} python import numpy as np +import torch from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer -from mqt.yaqs.characterization.memory.shared.encoding import encode_rho_pauli, unpack_rho8 -from mqt.yaqs.characterization.memory.shared.metrics import mean_trace_distance_rho8 - -PAULI_Z = np.array([[1, 0], [0, -1]], dtype=np.complex128) - - -def z_expectation(rho8_row: np.ndarray) -> float: - """Return ⟨Z⟩ from a packed 8-float density-matrix row.""" - rho = unpack_rho8(rho8_row) - return float(np.trace(PAULI_Z @ rho).real) - -length = 2 -ham = Hamiltonian.ising(length=length, J=1.0, g=1.0) -params = AnalogSimParams(dt=0.1) -mc = MemoryCharacterizer(show_progress=False) -num_interventions = 2 -timesteps = [0.0, 0.0, 0.0] -intervention_style = "measure_prepare" -``` -Use a **probe + environment** chain (`length >= 2`). Match `intervention_style` -and `timesteps` between training, `predict`, and `build_process_tensor` (length -`num_interventions + 1`). - -## Train a surrogate - -Training fixes `num_interventions` on the model — the horizon the network was -fit to. The settings below mirror the accuracy regression in the test suite -(`measure_prepare` legs, short schedule). - -```{code-cell} ipython3 -model = mc.train( - ham, - params, - num_interventions=num_interventions, - n=60, - seed=0, - timesteps=timesteps, - intervention_style=intervention_style, - train_kwargs={ - "epochs": 120, - "batch_size": 16, - "lr": 2e-3, - "device": "cpu", - "prefix_loss": "full", - }, - model_kwargs={ - "d_model": 32, - "nhead": 4, - "num_layers": 1, - "dim_ff": 64, - "dropout": 0.0, - }, -) +num_steps = 2 +interval = 0.6 +schedule = [0.0, interval, interval] +couplings = [0.3, 1.0] +field = 0.5 +hamiltonians = {coupling: Hamiltonian.ising(2, J=coupling, g=field) for coupling in couplings} +params = AnalogSimParams(elapsed_time=interval, dt=interval, preset="fast") +characterizer = MemoryCharacterizer(show_progress=False) ``` -## Evaluate on held-out Hamiltonian rollouts - -Generate fresh training sequences with a different seed and compare the -surrogate’s final-step predictions to the Hamiltonian targets used during -dataset construction: - -```{code-cell} ipython3 -held_out = mc.sample( - ham, - params, - num_interventions=num_interventions, - n=60, - seed=999, - intervention_style=intervention_style, - show_progress=False, - timesteps=timesteps, -) -e_test, rho0_test, rho_true = held_out.tensors -rho_pred = model.predict(e_test.numpy(), rho0_test.numpy(), return_numpy=True) - -z_true = np.array([z_expectation(row) for row in rho_true.numpy()[:, -1, :]]) -z_pred = np.array([z_expectation(row) for row in rho_pred[:, -1, :]]) -mean_td = mean_trace_distance_rho8(rho_pred[:, -1, :], rho_true.numpy()[:, -1, :]) - -fig, ax = plt.subplots(figsize=(4.5, 4)) -ax.scatter(z_true, z_pred, s=18, alpha=0.75) -lims = (min(z_true.min(), z_pred.min()) - 0.05, max(z_true.max(), z_pred.max()) + 0.05) -ax.plot(lims, lims, "k--", linewidth=1) -ax.set_xlim(lims) -ax.set_ylim(lims) -ax.set_xlabel(r"Hamiltonian $\langle Z \rangle$ (final step)") -ax.set_ylabel(r"Surrogate $\langle Z \rangle$ (final step)") -ax.set_title(rf"Held-out rollouts (mean trace distance = {mean_td:.3f})") -ax.set_aspect("equal") -fig.tight_layout() +`timesteps` contains one more duration than there are interventions. The initial +`0.0` means that the first intervention occurs immediately after preparation. +Evolution for $0.6$ follows each intervention, giving a final time of $1.2$ in +units with $\hbar=1$. These durations become part of the training problem; +`predict` does not accept a new time grid. + +We use the same schedule at weak coupling, $J=0.3$, and stronger coupling, +$J=1$. Each Hamiltonian needs its own model. Coupling strength is not an input +to the trained surrogate. The public training path also does not accept a +`NoiseModel`; this comparison changes environmental coupling rather than adding +a Lindblad noise channel. + +## 2. Train on random control sequences + +`sample` generates a validation dataset. `train` generates a separate training +dataset and fits the surrogate. Setting `intervention_style="haar"` draws random +single-qubit unitaries at both control times. The default initialization samples +a pure probe state from each random density matrix's eigenstates; the +environment remains in $|0\rangle$. + +```{code-cell} python +models = {} +for coupling, hamiltonian in hamiltonians.items(): + torch.manual_seed(7) + validation = characterizer.sample( + hamiltonian, params, num_interventions=num_steps, n=256, seed=99, + timesteps=schedule, intervention_style="haar", + ) + models[coupling] = characterizer.train( + hamiltonian, params, num_interventions=num_steps, n=4096, seed=7, + timesteps=schedule, intervention_style="haar", + model_kwargs={"d_model": 64, "num_layers": 2, "dim_ff": 128}, + train_kwargs={"epochs": 400, "lr": 1e-3, "device": "cpu", "val_dataset": validation}, + ) ``` -## Compare explicit control sequences - -The usual workflow after training is to call -{meth}`~mqt.yaqs.memory_characterizer.MemoryCharacterizer.predict` with -**your own control sequence** and compare outcomes across choices. Train a -one-leg surrogate on random unitary controls, then pass different explicit -unitary lists to the same model. This mirrors the quickstart workflow -({doc}`quickstart`) with the longer training budget used above: - -```{code-cell} ipython3 -unitary_timesteps = [0.0, 0.0] -controls_model = mc.train( - ham, - params, - num_interventions=1, - n=120, - seed=2, - timesteps=unitary_timesteps, - intervention_style="haar", - train_kwargs={ - "epochs": 120, - "batch_size": 16, - "lr": 2e-3, - "device": "cpu", - "prefix_loss": "full", - }, - model_kwargs={ - "d_model": 32, - "nhead": 4, - "num_layers": 1, - "dim_ff": 64, - "dropout": 0.0, - }, -) - -rho0_controls = np.eye(2, dtype=np.complex128) / 2.0 -hadamard = np.array([[1, 1], [1, -1]], dtype=np.complex128) / np.sqrt(2) -pauli_x = np.array([[0, 1], [1, 0]], dtype=np.complex128) -control_sequences = { - r"$\mathrm{H}$": [{"unitary": hadamard}], - r"$\mathrm{X}$": [{"unitary": pauli_x}], -} - -pauli_ops = { - "X": np.array([[0, 1], [1, 0]], dtype=np.complex128), - "Y": np.array([[0, -1j], [1j, 0]], dtype=np.complex128), - "Z": np.array([[1, 0], [0, -1]], dtype=np.complex128), -} -expectations = { - label: [ - float(np.trace(op @ mc.predict(controls_model, rho0_controls, controls, num_interventions=1)).real) - for op in pauli_ops.values() +The seeds separate training and validation sequences. At the end of training, +YAQS restores the model with the lowest validation loss across the 400 epochs. +The validation set therefore selects the model; it is not an independent test of +the predictions below. The small architecture and CPU setting bound this +example's training cost. Other hardware, seeds, and PyTorch versions can give +different errors. + +The resulting model learns a mapping from the initial probe state and control +sequence to reduced probe states. It does not reconstruct the environment's +state. Both the environment preparation and the two-intervention horizon stay +fixed throughout this example. + +## 3. Predict the response to a chosen pulse + +Prepare the probe in $|+\rangle=(|0\rangle+|1\rangle)/\sqrt{2}$. Apply the +identity at the first control time, let the joint system evolve for $0.6$, then +apply $R_z(\theta)$ to the probe. The surrogate predicts its state after the +second evolution interval. We sweep the angle while using the same model. + +```{code-cell} python +plus = np.array([1, 1], dtype=complex) / np.sqrt(2) +rho0 = np.outer(plus, plus.conj()) +identity = np.eye(2, dtype=complex) +pulse_angles = np.linspace(0, 2 * np.pi, 61) +sequences = [ + [ + {"unitary": identity}, + {"unitary": np.diag(np.exp(-0.5j * angle * np.array([1, -1])))}, ] - for label, controls in control_sequences.items() + for angle in pulse_angles +] +predictions = { + coupling: np.stack([characterizer.predict(model, rho0, sequence) for sequence in sequences]) + for coupling, model in models.items() } +``` -pauli_names = list(pauli_ops) -x = np.arange(len(pauli_names)) -width = 0.35 - -fig, ax = plt.subplots(figsize=(5.5, 3.5)) -for offset, (label, values) in zip((-width / 2, width / 2), expectations.items()): - ax.bar(x + offset, values, width, label=f"control {label}") -ax.set_xticks(x, pauli_names) -ax.set_ylabel(r"$\langle P \rangle$") -ax.set_title("Probe Pauli expectations for two control sequences") -ax.legend(frameon=False) -fig.tight_layout() +`predict` returns a complex array of shape `(2, 2)`. Each stacked sweep has +shape `(61, 2, 2)`, with the first axis following `pulse_angles`. These chosen +sequences were not supplied during training; the model must generalize from its +random controls. At $\theta=0$ the sequence gives free evolution, and +$\theta=\pi$ gives a phase flip halfway through the evolution. + +The following figure uses the stronger coupling. Its left panel projects the +final states onto the equatorial Bloch plane, with coordinates +$(\langle X\rangle,\langle Y\rangle)$. The right panel plots coherence +$C=2|\rho_{01}|$, which is the distance from the origin in that plane for a +physical qubit state. All points describe the same final time; the colored curve +is a control-angle sweep, not a trajectory through time. + +```{code-cell} python +:tags: [hide-input] +import matplotlib.pyplot as plt +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", "font.serif": ["STIXGeneral"], "mathtext.fontset": "stix", + "font.size": 10, "axes.labelsize": 11, "axes.linewidth": 0.7, + "xtick.direction": "in", "ytick.direction": "in", + "xtick.top": True, "ytick.right": True, + "legend.frameon": False, "figure.constrained_layout.use": True, + "savefig.bbox": "tight", "svg.fonttype": "none", +}) +from matplotlib.collections import LineCollection +from matplotlib.patches import Circle + +predicted_states = predictions[1.0] +predicted_coherence = 2 * np.abs(predicted_states[:, 0, 1]) +no_pulse_coherence = predicted_coherence[0] +fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.4), gridspec_kw={"width_ratios": [1, 1.4]}) + +# Show the final states projected onto the equatorial Bloch plane. +bloch_xy = np.column_stack(( + 2 * predicted_states[:, 0, 1].real, + -2 * predicted_states[:, 0, 1].imag, +)) +points = bloch_xy[:, None, :] +segments = np.concatenate((points[:-1], points[1:]), axis=1) +trajectory = LineCollection(segments, cmap="twilight_shifted", + norm=plt.Normalize(0, 2 * np.pi), linewidth=2.6) +trajectory.set_array((pulse_angles[:-1] + pulse_angles[1:]) / 2) +axes[0].add_patch(Circle((0, 0), 1, facecolor="0.97", edgecolor="0.75", linewidth=0.8)) +axes[0].add_patch(Circle((0, 0), 0.5, fill=False, edgecolor="0.85", linewidth=0.6)) +axes[0].axhline(0, color="0.85", linewidth=0.6) +axes[0].axvline(0, color="0.85", linewidth=0.6) +axes[0].add_collection(trajectory) +axes[0].plot(*bloch_xy[0], "o", color="0.3", markerfacecolor="white", markersize=6) +axes[0].set(xlabel=r"$\langle X\rangle$", ylabel=r"$\langle Y\rangle$", + xlim=(-1.05, 1.05), ylim=(-1.05, 1.05), aspect="equal", + xticks=[-1, 0, 1], yticks=[-1, 0, 1]) +axes[0].set_title("(a) Final probe state", loc="left", fontsize=11) +colorbar = fig.colorbar(trajectory, ax=axes[0], orientation="horizontal", + shrink=0.8, pad=0.08, aspect=25, ticks=[0, np.pi, 2 * np.pi]) +colorbar.ax.set_xticklabels(["0", r"$\pi$", r"$2\pi$"]) +colorbar.set_label(r"Pulse angle $\theta$") + +axes[1].fill_between(pulse_angles, no_pulse_coherence, predicted_coherence, + where=predicted_coherence >= no_pulse_coherence, + interpolate=True, color="#0072B2", alpha=0.15) +axes[1].fill_between(pulse_angles, no_pulse_coherence, predicted_coherence, + where=predicted_coherence < no_pulse_coherence, + interpolate=True, color="#D55E00", alpha=0.2) +axes[1].plot(pulse_angles, predicted_coherence, color="#0072B2", linewidth=2.2, + label="With control pulse") +axes[1].axhline(no_pulse_coherence, color="0.4", linestyle="--", linewidth=1.1, + label="Free evolution") +axes[1].set(xlabel=r"Pulse angle $\theta$", ylabel=r"Final coherence $2|\rho_{01}|$", + xlim=(0, 2 * np.pi), ylim=(0, 1), + xticks=[0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi], + xticklabels=["0", r"$\pi/2$", r"$\pi$", r"$3\pi/2$", r"$2\pi$"]) +axes[1].set_title("(b) Predicted coherence", loc="left", fontsize=11) +axes[1].legend(loc="upper right", fontsize=9) +plt.show() ``` -Extend the per-leg list when `num_interventions > 1` to probe multi-step -sequences (for example `[H, X]`). +**A control pulse changes the final coherence.** The open circle marks free +evolution. Shading shows changes relative to that prediction. An instantaneous +$Z$ rotation preserves coherence magnitude at the moment it is applied; the +differences here arise during the subsequent joint evolution. A prediction alone +does not show whether the model has learned that response accurately, so we next +compare it with a reference. (short-horizon-validation)= -## Validate against exact references - -Build process tensors for the same schedule. By default, `build_process_tensor` -returns an uncapped MPO from direct construction (noiseless). Its branch count -grows as `16**num_interventions`, so use it only for short horizons. A finite -`max_bond_dim` selects an experimental uncontrolled approximation and emits a -`RuntimeWarning`. Pass `return_type="dense"` for exhaustive tomography. For -process tensors, `rho0` in `predict` must match `pt.initial_rho` (the site-0 -state after the initial leg of the reference schedule). - -**Dense** and **MPO** implementations should agree on identical interventions. -Compare all three backends on a -**stochastic sequence drawn from the training style** (`measure_prepare` here): -pass a **fresh** `np.random.default_rng(seed)` to each `predict` call (reusing -one RNG object advances its state between calls). - -```{code-cell} ipython3 -pt_mpo = mc.build_process_tensor(ham, params, timesteps=timesteps) -pt_dense = mc.build_process_tensor( - ham, params, timesteps=timesteps, return_type="dense", num_trajectories=48, -) - -rho0 = pt_mpo.initial_rho -compare_seed = 7 - -rho_dense = mc.predict( - pt_dense, rho0, intervention_style, num_interventions=num_interventions, - rng=np.random.default_rng(compare_seed), -) -rho_mpo = mc.predict( - pt_mpo, rho0, intervention_style, num_interventions=num_interventions, - rng=np.random.default_rng(compare_seed), -) -rho_surrogate = mc.predict( - model, rho0, intervention_style, num_interventions=num_interventions, - rng=np.random.default_rng(compare_seed), -) - -pauli_labels = [r"$X$", r"$Y$", r"$Z$"] -pauli_dense = encode_rho_pauli(rho_dense)[1:] -pauli_mpo = encode_rho_pauli(rho_mpo)[1:] -pauli_surrogate = encode_rho_pauli(rho_surrogate)[1:] -x = np.arange(len(pauli_labels)) -width = 0.25 - -fig, ax = plt.subplots(figsize=(5.5, 3.5)) -ax.bar(x - width, pauli_dense, width, label="dense", color="tab:blue") -ax.bar(x, pauli_mpo, width, label="MPO", color="tab:orange", alpha=0.85) -ax.bar(x + width, pauli_surrogate, width, label="surrogate", color="tab:green", alpha=0.85) -ax.set_xticks(x, pauli_labels) -ax.set_ylabel(r"$\langle P \rangle$") -ax.set_title( - rf"Matched {intervention_style} sequence ($\|\rho_{{\mathrm{{dense}}}}-\rho_{{\mathrm{{MPO}}}}\|_F$ = " - rf"{np.linalg.norm(rho_dense - rho_mpo):.1e})" -) -ax.legend(frameon=False) -fig.tight_layout() +## 4. Check the predictions against joint evolution + +For two qubits, we can build the Hamiltonian directly with NumPy and propagate +with SciPy's matrix exponential. This reference uses neither the surrogate nor +YAQS's evolution routines. With site 0 as the least significant bit, the joint +initial vector is $|0\rangle_{\mathrm{env}}\otimes|+\rangle_{\mathrm{probe}}$, +and a probe rotation acts as $I\otimes R_z(\theta)$. + +```{code-cell} python +from scipy.linalg import expm + +pauli_x = np.array([[0, 1], [1, 0]], dtype=complex) +pauli_z = np.diag([1.0, -1.0]) +initial_joint = np.kron([1, 0], plus) +references = {} +for coupling in couplings: + dense_hamiltonian = -coupling * np.kron(pauli_z, pauli_z) - field * ( + np.kron(identity, pauli_x) + np.kron(pauli_x, identity) + ) + evolution = expm(-1j * interval * dense_hamiltonian) + states = [] + for sequence in sequences: + pulse = sequence[1]["unitary"] + joint = evolution @ np.kron(identity, pulse) @ evolution @ initial_joint + amplitudes = joint.reshape(2, 2) + states.append(amplitudes.T @ amplitudes.conj()) + references[coupling] = np.stack(states) ``` -Dense and MPO should overlap; the surrogate approximates the same draw at the -reference `rho0`. Held-out Hamiltonian rollouts above use random probe `rho0` -values from data generation — a different setup than the fixed reference state -stored on process tensors. - -The same process-Choi information functionals are available on either backend. -QMI measures total correlation between the final output and the selected -intervention slots, including direct system transmission. It is not by itself a -measure of non-Markovian memory. CMI tests conditional independence for the -stated partition. For this short horizon, CMI is near zero while QMI grows when -more past legs are included: - -```{code-cell} ipython3 -past_choices = ("all", "first", "last") -qmi_dense = [mc.compute_qmi(pt_dense, past=p) for p in past_choices] -qmi_mpo = [mc.compute_qmi(pt_mpo, past=p) for p in past_choices] -cmi_dense = mc.compute_cmi(pt_dense) -cmi_mpo = mc.compute_cmi(pt_mpo) - -fig, ax = plt.subplots(figsize=(5, 3.5)) -ax.plot(past_choices, qmi_dense, "o-", label="QMI (dense)") -ax.plot(past_choices, qmi_mpo, "s--", label="QMI (MPO)", alpha=0.85) -ax.axhline(cmi_dense, color="tab:purple", linestyle=":", linewidth=1.5, label=rf"CMI (dense) = {cmi_dense:.2e}") -ax.axhline(cmi_mpo, color="tab:gray", linestyle="--", linewidth=1, label=rf"CMI (MPO) = {cmi_mpo:.2e}") -ax.set_ylabel("bits") -ax.set_xlabel(r"past legs in QMI") -ax.set_title("Process-tensor information metrics") -ax.legend(frameon=False, fontsize=8, loc="upper left") -fig.tight_layout() +Tracing out the environment gives the reference probe density matrix. Compare +the full matrix as well as the plotted coherence. We use half the trace norm of +the matrix difference, which equals trace distance when both matrices are +normalized physical states. + +```{code-cell} python +matrix_errors = {} +for coupling in couplings: + predicted = predictions[coupling] + reference = references[coupling] + matrix_errors[coupling] = 0.5 * np.sum(np.abs(np.linalg.eigvalsh(predicted - reference)), axis=1) + trace_error = np.max(np.abs(np.trace(predicted, axis1=1, axis2=2) - 1)) + hermiticity_error = np.max(np.abs(predicted - predicted.conj().swapaxes(1, 2))) + minimum_eigenvalue = np.min(np.linalg.eigvalsh(predicted)) + coherence_rmse = np.sqrt(np.mean((2 * np.abs(predicted[:, 0, 1]) - 2 * np.abs(reference[:, 0, 1])) ** 2)) + print( + f"J={coupling:g}: max matrix error={matrix_errors[coupling].max():.4f}, " + f"coherence RMSE={coherence_rmse:.4f}\n" + f" max trace error={trace_error:.4f}, " + f"Hermiticity error={hermiticity_error:.1e}, min eigenvalue={minimum_eigenvalue:.4f}" + ) ``` -These information metrics and the split-cut response metric $S_V(c)$ from -{doc}`characterization` are complementary. The response construction tests which -differences between past probes remain visible in future responses. - -## Related topics +The public API makes each returned estimate Hermitian, so a zero Hermiticity +error is expected. It does not enforce unit trace or positivity. The printed +checks expose normalization error and any negative eigenvalues; we do not +renormalize the predictions, clip their eigenvalues, or project them onto +physical states. A positive minimum eigenvalue alone is insufficient when the +trace differs from one. + +## 5. Compare environmental couplings + +The second model asks whether the same control has a different effect when the +probe couples more weakly to its environment. Plot both coherence sweeps with +their independent references, then show where the full predicted matrices differ +from those references. + +```{code-cell} python +:tags: [hide-input] +fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.1)) +colors = ["#D55E00", "#0072B2"] +for coupling, color in zip(couplings, colors, strict=True): + coherence = 2 * np.abs(predictions[coupling][:, 0, 1]) + exact_coherence = 2 * np.abs(references[coupling][:, 0, 1]) + axes[0].plot(pulse_angles, coherence, color=color, linewidth=2, label=rf"Surrogate, $J={coupling:g}$") + axes[0].plot(pulse_angles[::5], exact_coherence[::5], "o", color=color, + markerfacecolor="white", markersize=4, markeredgewidth=1) + axes[1].plot(pulse_angles, matrix_errors[coupling], color=color, linewidth=2, label=rf"$J={coupling:g}$") +axes[0].plot([], [], "o", color="0.3", markerfacecolor="white", markersize=4, label="Joint evolution") +axes[0].set(ylabel=r"Final coherence $2|\rho_{01}|$") +axes[1].set(ylabel=r"Matrix error $\frac{1}{2}\|\rho_{\rm pred}-\rho_{\rm ref}\|_1$") +for ax in axes: + ax.set(xlabel=r"Pulse angle $\theta$", xlim=(0, 2 * np.pi), + xticks=[0, np.pi, 2 * np.pi], xticklabels=["0", r"$\pi$", r"$2\pi$"]) + ax.legend(fontsize=8, loc="best") +axes[1].set_ylim(bottom=0) +axes[0].set_title("(a) Coupling changes the control response", loc="left", fontsize=10) +axes[1].set_title("(b) Error against joint evolution", loc="left", fontsize=10) +plt.show() +``` -- {doc}`characterization` — split-cut probing, response matrix, reset-delay - sweeps -- {doc}`quickstart` — minimal train/predict snippet -- API reference: :class:`~mqt.yaqs.memory_characterizer.MemoryCharacterizer` +**The same pulse produces different responses at the two couplings.** Solid +curves show surrogate predictions; open circles show direct evolution. The error +panel and printed state checks bound what we can infer from those curves. +Accuracy on random validation sequences does not certify a chosen pulse family, +and this two-step comparison says nothing about longer sequences. Repeat the +reference checks when changing the Hamiltonian, environment preparation, control +family, or time horizon. + +## Other supported options + +`predict(model, rho0, sequence, return_sequence=True)` returns an array of shape +`(num_interventions, 2, 2)`. Its entries describe the probe after each +intervention and its following evolution interval, not a continuous time trace. +Predictions for earlier steps still use the trained schedule. + +For other control families, `intervention_style` also accepts `"clifford"` and +`"measure_prepare"` when sampling or training. Prediction sequences accept +unitary dictionaries as above, or style strings that draw random controls. A +`"measure_prepare"` draw selects a rank-one measurement outcome and a +replacement state. Match the training controls to the intended queries and +validate other interventions separately; this example tests only unitary +controls. See {doc}`characterization` for memory diagnostics. + +Short process tensors provide another reference through `build_process_tensor` +and the same `predict` call. They start from the joint all-zero state, and their +input must match `process_tensor.initial_rho`. They therefore do not directly +match the $|+\rangle$ preparation used here. The default uncapped MPO +construction grows as `16**num_interventions`; dense tomography also grows +exponentially. Use these references for short horizons and see +{doc}`characterization` for approximation limits, QMI, and CMI. Those +process-tensor diagnostics are distinct from prediction accuracy and from the +response-matrix memory spectrum. From 8bf89fc0fa3f09dbcd29fda11d8b20ebebb0b528 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:06:20 +0200 Subject: [PATCH 12/30] updated analog digital --- docs/examples/digital_analog_simulation.md | 528 ++++++++++----------- 1 file changed, 264 insertions(+), 264 deletions(-) diff --git a/docs/examples/digital_analog_simulation.md b/docs/examples/digital_analog_simulation.md index 59018a197..3d9fb5f64 100644 --- a/docs/examples/digital_analog_simulation.md +++ b/docs/examples/digital_analog_simulation.md @@ -2,312 +2,312 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true - execution_timeout: 600 + execution_timeout: 300 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` +# Analog-Digital Simulation -# Digital–Analog Simulation +An excitation spreads through the XY chain in {doc}`analog_simulation`. Can +digital gates bring it back? Here we interrupt that same continuous Hamiltonian +evolution with a pattern of phase gates. The excitation refocuses at its +starting site, giving a direct way to see how relaxation and dephasing affect +the return. -Some simulations need both analog parts (continuous Hamiltonian evolution) and -digital parts (quantum circuits). A YAQS {class}`~mqt.yaqs.SimulationProgram` -accepts an ordered list of `(operator, params)` pairs and runs them as one -program. YAQS passes the evolving state from one segment to the next, keeps each -noisy trajectory continuous across segment boundaries, and returns a normal -{class}`~mqt.yaqs.Result` with stitched `times` and `expectation_values`. +A `SimulationProgram` combines circuits and analog intervals in their execution +order. YAQS carries the evolving state through the whole program, including each +noisy trajectory. This guide uses the standard installation and Matplotlib for +plotting. Run the cells in order in a notebook; for a script, use the +entry-point guard in {doc}`simulator_initialization`. -To demonstrate this workflow, the following example: +## 1. Prepare the chain with a circuit -1. prepares a phase-sensitive state with a digital operation; -2. evolves it continuously under a simple static-$Z$ Hamiltonian that - accumulates phase; -3. optionally inserts an instantaneous digital pulse between two analog parts; -4. runs both programs with and without noise. +Use the same 20-site open XY chain and hopping amplitude as the analog and +{doc}`circuit_observables` guides, -## 1. Create the digital operations +$$ +H=-\frac{1}{2}\sum_{i=0}^{L-2}(X_iX_{i+1}+Y_iY_{i+1}). +$$ -One qubit is enough for this demonstration. A Hadamard gate prepares the -phase-sensitive state $\lvert+\rangle$ from the initial state $\lvert0\rangle$ -(`"zeros"`, specified later on). An $X$ gate will act as the midpoint refocusing -pulse and, when repeated at the end, return the sequence to its original frame. +Time is measured in inverse hopping units, with $\hbar=1$. Start from an +all-zero MPS, then use a digital $X$ gate to prepare one excitation at site 10. +Site numbers match Qiskit's qubit indices. -```{code-cell} ipython3 -from qiskit.circuit import QuantumCircuit +```{code-cell} python +import numpy as np +from qiskit import QuantumCircuit -number_of_qubits = 1 +from mqt.yaqs import AnalogSimParams, DigitalSimParams, Hamiltonian, Observable, SimulationProgram, Simulator, State -preparation = QuantumCircuit(number_of_qubits) -preparation.h(0); +length = 20 +center = length // 2 +state = State(length, initial="zeros") +hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) +observables = [Observable("z", site) for site in range(length)] -refocusing_pulse = QuantumCircuit(number_of_qubits) -refocusing_pulse.x(0); +preparation = QuantumCircuit(length) +preparation.x(center) ``` -These are normal Qiskit circuits. They will appear as the first entry of each -digital pair in the program. - -## 2. Configure the parameters +Unlike the circuit guide, we will let YAQS evolve the Hamiltonian directly +between gates. We need no Trotter circuit to represent those intervals. -Segment parameters carry timing, truncation, gate-mode, and digital `shots`. -Observables, `random_seed`, and `get_state` belong on the program; `num_traj` is -either unanimous on the segments or set on the program. +## 2. Build the refocusing pulse -```{code-cell} ipython3 -from mqt.yaqs import AnalogSimParams, DigitalSimParams, Hamiltonian, Observable +After half the evolution, apply a $Z$ gate to every even site. The pulse changes +phases without changing site occupations at that instant. Every XY bond has +exactly one pulsed endpoint, so the combined pulse $P$ satisfies $PHP=-H$. The +subsequent evolution therefore unwinds the earlier spreading. -half_duration = 0.7 -number_of_trajectories = 256 +```{code-cell} python +phase_pulse = QuantumCircuit(length) +phase_pulse.z(range(0, length, 2)) -hamiltonian = Hamiltonian.pauli( - length=number_of_qubits, - one_body=[(1.1, "z")], -) -x_observable = Observable("x", 0) -analog_parameters = AnalogSimParams( - elapsed_time=half_duration, - dt=0.05, - sample_timesteps=True, -) -digital_parameters = DigitalSimParams(sample_layers=True) +half_duration = 1.5 +analog_params = AnalogSimParams(elapsed_time=half_duration, dt=0.25, order=2, preset="fast") +digital_params = DigitalSimParams(sample_layers=True, preset="fast") ``` -`elapsed_time` is the duration of each analog segment, while `dt` controls its -time-step size. With `sample_layers=True`, digital segments also record the -shared observable at circuit entry and exit. +The two analog intervals give a final time of $3$, matching the other transport +guides. `dt=0.25` samples seven times per interval. We use second-order TJM +evolution for the noisy comparison. `sample_layers=True` also records the +observables at each circuit's entry and exit. -## 3. Put the operations into programs +The phase pulse reverses this XY Hamiltonian because it changes the sign of +every exchange term. Added terms such as ZZ interactions generally do not +reverse under the same pulse; another Hamiltonian needs a separate check. -A program is an ordered list of `(operator, params)` pairs. The operator type -selects the mode: a {class}`~qiskit.circuit.QuantumCircuit` (or an OpenQASM -string / file path) with {class}`~mqt.yaqs.DigitalSimParams`, or a -{class}`~mqt.yaqs.Hamiltonian` with {class}`~mqt.yaqs.AnalogSimParams`. +## 3. Assemble and run the programs -```{code-cell} ipython3 -from mqt.yaqs import SimulationProgram +Each segment is an `(operator, params)` pair. A circuit selects digital +simulation, while a `Hamiltonian` selects analog evolution. Set the shared +observables and random seed on the program; leave those fields unset on the +segment parameters. We also set the trajectory budget on the program so it +applies to the complete sequence. -free_evolution = SimulationProgram( +```{code-cell} python +free_program = SimulationProgram( [ - (preparation, digital_parameters), - (hamiltonian, analog_parameters), - (hamiltonian, analog_parameters), + (preparation, digital_params), + (hamiltonian, analog_params), + (hamiltonian, analog_params), ], - observables=[x_observable], - num_traj=number_of_trajectories, - random_seed=7, + observables=observables, num_traj=32, random_seed=7, ) -``` - -The second program inserts the refocusing pulse between the analog intervals and -repeats it at the end as a frame correction. - -```{code-cell} ipython3 -evolution_with_hahn_echo = SimulationProgram( +echo_program = SimulationProgram( [ - (preparation, digital_parameters), - (hamiltonian, analog_parameters), - (refocusing_pulse, digital_parameters), - (hamiltonian, analog_parameters), - (refocusing_pulse, digital_parameters), + (preparation, digital_params), + (hamiltonian, analog_params), + (phase_pulse, digital_params), + (hamiltonian, analog_params), + (phase_pulse, digital_params), ], - observables=[x_observable], - num_traj=number_of_trajectories, - random_seed=7, + observables=observables, num_traj=32, random_seed=7, ) -``` - -There is no need to run these segments individually or manually extract and -resubmit an intermediate state. YAQS carries the state through each complete -list in order. - -## 4. Run the programs with and without noise - -Create a simulator and an initial state, then pass each program to -{meth}`~mqt.yaqs.Simulator.run`. - -```{code-cell} ipython3 -from mqt.yaqs import NoiseModel, Simulator, State -simulator = Simulator(parallel=False, show_progress=False) -initial_state = State(number_of_qubits, initial="zeros") - -free_noiseless = simulator.run(initial_state, free_evolution) -echo_noiseless = simulator.run(initial_state, evolution_with_hahn_echo) +simulator = Simulator(show_progress=False) +free_result = simulator.run(state, free_program) +echo_result = simulator.run(state, echo_program) ``` -Adding noise does not require changing either program. Pass YAQS's built-in -Markovian `pauli_z` dephasing process to the same calls. - -```{code-cell} ipython3 -dephasing_noise = NoiseModel( - [{"name": "pauli_z", "sites": [0], "strength": 0.15}] -) - -free_noisy = simulator.run( - initial_state, - free_evolution, - noise_model=dephasing_noise, -) -echo_noisy = simulator.run( - initial_state, - evolution_with_hahn_echo, - noise_model=dephasing_noise, -) -``` - -During a noisy run, each trajectory passes through the complete program on one -worker (one MPS and one RNG) before YAQS averages the recorded observables. -Parallelism is over trajectories, not over segments. - -You can also pass the pair list directly to `run`, which builds a -`SimulationProgram` under the hood: - -```python -result = simulator.run( - initial_state, - [(preparation, digital_parameters), (hamiltonian, analog_parameters)], - observables=[x_observable], - num_traj=number_of_trajectories, - random_seed=7, -) +The final phase pulse restores the original frame. With $U(\tau)=\exp(-iH\tau)$, +the echo part obeys $PU(\tau)PU(\tau)=I$ in the noiseless limit. This last pulse +does not change the plotted occupations, but it also restores the phases of a +general initial state. + +YAQS preserves the input `state`, so both programs start from the same all-zero +state and apply the same preparation. Noiseless programs need only one +trajectory. Noisy programs average 32 complete trajectories, with parallel +execution enabled by default. The documentation suppresses progress bars; omit +`show_progress=False` to see them. + +## 4. Read the occupation heatmaps + +The outer result contains stitched `times` and `expectation_values`. Digital +gates are instantaneous on this timeline, so their samples share timestamps with +analog boundaries. Each entry in `segment_results` also contains the segment's +type, time offset, and local output. + +For heatmaps, collect the analog samples, add each segment's time offset, and +remove the repeated midpoint. The $Z$ pulse leaves occupation unchanged there, +so either adjacent analog sample gives the same value. The initial analog sample +already includes the digital preparation. + +```{code-cell} python +def analog_occupations(result): + """Extract analog times and site occupations from a program result.""" + segments = [segment for segment in result.segment_results if segment.segment_type == "analog"] + times = np.concatenate([segment.times + segment.time_offset for segment in segments]) + values = np.concatenate([np.asarray(segment.expectation_values) for segment in segments], axis=1) + keep = np.r_[True, np.diff(times) > 0] + return times[keep], (1 - values[:, keep]) / 2 + + +times, free_occupation = analog_occupations(free_result) +_, echo_occupation = analog_occupations(echo_result) +print(f"Return occupation: free={free_occupation[center, -1]:.4f}, echo={echo_occupation[center, -1]:.4f}") ``` -## 5. OpenQASM, scheduled jumps, and per-segment results +The occupation array has shape `(20, 13)`. Rows follow the observable list, and +columns follow the extracted times from $0$ to $3$. -Programs reuse the same **operator / params / noise** objects as MPS TJM analog -and digital runs. A few knobs are program-owned or unsupported here: the initial -state must be MPS; observables, `random_seed`, and `get_state` belong on the -program; `multi_time_observables` are not supported. - -**Digital operators.** A segment may take a -{class}`~qiskit.circuit.QuantumCircuit`, or an OpenQASM string / file path, with -{class}`~mqt.yaqs.DigitalSimParams`. `shots` stay on the digital segment params: - -```python -program = SimulationProgram( - [("prep.qasm", digital_parameters), (hamiltonian, analog_parameters)], - observables=[x_observable], -) -``` - -**Scheduled jumps.** Attach them through a {class}`~mqt.yaqs.NoiseModel` as in -{doc}`scheduled_jumps` (analog MPS TJM, `order=1`, times on that analog run's -`dt` grid). Jump times are relative to the start of the analog run that carries -the model. Consecutive analog segments that share one interval schedule also -share that clock; a digital gate starts a new analog run. Use a third-tuple -noise override when only one segment should fire them: - -```python -jumps = NoiseModel(scheduled_jumps=[{"time": 0.1, "sites": [0], "name": "x"}]) -program = SimulationProgram( - [ - (preparation, digital_parameters), - (hamiltonian, analog_parameters, jumps), - ], - observables=[x_observable], -) +```{code-cell} python +:tags: [hide-input] +import matplotlib.pyplot as plt +from matplotlib.colors import PowerNorm +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", "font.serif": ["STIXGeneral"], "mathtext.fontset": "stix", + "font.size": 10, "axes.labelsize": 11, "axes.linewidth": 0.8, + "xtick.direction": "in", "ytick.direction": "in", "svg.fonttype": "none", + "legend.frameon": False, +}) +occupation_norm = PowerNorm(gamma=0.5, vmin=0, vmax=1) +fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.8), sharex=True, sharey=True, layout="constrained") +for ax, occupation, title in zip( + axes, [free_occupation, echo_occupation], ["(a) Free evolution", "(b) Digital refocusing"], strict=True, +): + image = ax.pcolormesh(times, np.arange(length), occupation, shading="auto", cmap="cividis", norm=occupation_norm) + ax.set(xlabel="Time", title=title, yticks=[0, 5, 10, 15, 19], xlim=(0, 3)) +axes[0].set_ylabel("Site") +axes[1].axvline(half_duration, color="white", linestyle="--", linewidth=1) +fig.colorbar(image, ax=axes, label=r"Occupation $\langle n_i\rangle$", ticks=[0, 0.25, 0.5, 1], shrink=0.9) +plt.show() ``` -**Trajectory count.** Set `num_traj=` on the program, or use the same `num_traj` -on every segment. If segments disagree, pass an explicit program value. - -**Results.** The outer result stitches `result.times` and -`result.expectation_values`. Outer `result.counts` is the histogram from the -last segment that recorded shots. Each segment also keeps an ordinary -{class}`~mqt.yaqs.Result` at `result.segment_results[i]`. - -## 6. Compare the four results - -Program results look like ordinary analog/digital results: use `result.times` -and `result.expectation_values`. Digital samples sit at their physical time -offset (operations are instantaneous on the program timeline), so repeated -timestamps around pulses are expected. - -```{code-cell} ipython3 -times = echo_noiseless.times -signal = echo_noiseless.expectation_values[0] +**The phase pulse brings the spreading excitation back.** Both programs follow +the same dynamics until $t=1.5$. Free evolution continues to spread, while the +pulsed program refocuses at site 10 at $t=3$. The white dashed line marks the +midpoint pulse. Both panels use the same square-root color scale to retain weak +occupation without changing the normalization. + +## 5. Add relaxation and dephasing + +A pulse can reverse coherent spreading, but it cannot reverse an irreversible +noise process. Uniform relaxation removes the excitation. Local dephasing +preserves total excitation while disrupting the phases needed for refocusing. +Apply each noise model to the same echo program. + +```{code-cell} python +from mqt.yaqs import NoiseModel + +relaxation_rate = 0.5 +dephasing_rates = [0.05, 0.2] +noise_models = { + "Relaxation": NoiseModel([ + {"name": "lowering", "sites": [site], "strength": relaxation_rate} + for site in range(length) + ]), + **{ + f"Dephasing {rate:g}": NoiseModel([ + {"name": "pauli_z", "sites": [site], "strength": rate} + for site in range(length) + ]) + for rate in dephasing_rates + }, +} +echo_results = {"No noise": echo_result} +for label, noise in noise_models.items(): + echo_results[label] = simulator.run(state, echo_program, noise_model=noise) + +occupations = {label: analog_occupations(result)[1] for label, result in echo_results.items()} ``` -We now plot the results, indicating noisy simulation with dashed lines. - -```{code-cell} ipython3 ---- -mystnb: - image: - width: 80% - align: center ---- -import matplotlib.pyplot as plt - -colors = plt.colormaps["viridis"]([0.2, 0.75]) -traces = [ - (free_noiseless, "free evolution, noiseless", colors[0], "-"), - (echo_noiseless, "with digital pulses, noiseless", colors[1], "-"), - (free_noisy, "free evolution, noisy", colors[0], "--"), - (echo_noisy, "with digital pulses, noisy", colors[1], "--"), -] - -fig, ax = plt.subplots(figsize=(7, 4), layout="constrained") -for result, label, color, line_style in traces: - ax.plot( - result.times, - result.expectation_values[0], - "o", - color=color, - linestyle=line_style, - markersize=2.5, - label=label, - ) - -ax.axvline(half_duration, color="0.65", linewidth=1, label="digital pulse") -ax.set( - xlabel="Time", - ylabel=r"Phase-sensitive signal $\langle X\rangle$", - ylim=(-1.05, 1.05), -) -ax.legend(ncols=2) +`strength` is a Lindblad rate in inverse time. The jump operators are +$\sqrt{\gamma_-}\,|0\rangle\langle1|_i$ for relaxation and +$\sqrt{\gamma_z}\,Z_i$ for dephasing. In this convention an isolated qubit's +off-diagonal density-matrix entries decay at rate $2\gamma_z$. + +The run-level noise model is inherited by all segments. These one-qubit gates +receive no stochastic circuit noise in YAQS, so noise acts only during the +analog intervals here. The pulses are ideal and instantaneous. The same noisy +trajectory and random stream continue across the midpoint pulse. + +For a single excitation with uniform relaxation, the exact total population is +$N(t)=\exp(-\gamma_-t)$. Surviving trajectories still refocus, so the exact +final return occupation has the same value, about $0.223$ at $t=3$. Under +Pauli-Z dephasing, $N(t)=1$, but the return becomes weaker and occupation +remains away from the center. This comparison uses different noise channels; +their numerical rates do not represent equal physical error strengths. + +```{code-cell} python +:tags: [hide-input] +labels = list(echo_results) +colors = ["0.15", "#D55E00", "#56B4E9", "#0072B2"] +fig, axes = plt.subplots(3, 2, figsize=(7.2, 6.8), layout="constrained") +titles = ["(a) No noise", r"(b) Relaxation, $\gamma_-=0.5$", + r"(c) Dephasing, $\gamma_z=0.05$", r"(d) Dephasing, $\gamma_z=0.2$"] +for ax, label, title in zip(axes[:2].flat, labels, titles, strict=True): + image = ax.pcolormesh(times, np.arange(length), occupations[label], shading="auto", cmap="cividis", norm=occupation_norm) + ax.axvline(half_duration, color="white", linestyle="--", linewidth=1) + ax.set(xlabel="Time", ylabel="Site", title=title, yticks=[0, 10, 19], xlim=(0, 3)) +fig.colorbar(image, ax=list(axes[:2].flat), label=r"Occupation $\langle n_i\rangle$", + ticks=[0, 0.25, 0.5, 1], shrink=0.8) + +for label, color in zip(labels, colors, strict=True): + occupation = occupations[label] + result = echo_results[label] + segments = [segment for segment in result.segment_results if segment.segment_type == "analog"] + raw_times = np.concatenate([segment.times + segment.time_offset for segment in segments]) + keep = np.r_[True, np.diff(raw_times) > 0] + trajectories = (1 - np.concatenate([np.asarray(segment.trajectories) for segment in segments], axis=-1)) / 2 + trajectories = trajectories[..., keep] + for ax, mean, samples in ( + (axes[2, 0], occupation[center], trajectories[center]), + (axes[2, 1], occupation.sum(axis=0), trajectories.sum(axis=0)), + ): + ax.plot(times, mean, color=color, linewidth=1.7, label=label) + if samples.shape[0] > 1: + standard_error = samples.std(axis=0, ddof=1) / np.sqrt(samples.shape[0]) + ax.fill_between(times, mean - standard_error, mean + standard_error, color=color, alpha=0.15) +axes[2, 1].plot(times, np.exp(-relaxation_rate * times), ":", color="#D55E00", linewidth=1.4, label=r"$e^{-\gamma_-t}$") +for ax in axes[2]: + ax.axvline(half_duration, color="0.6", linestyle="--", linewidth=0.8) + ax.set(xlabel="Time", xlim=(0, 3)) +axes[2, 0].set(title="(e) Return to the center", ylabel=r"$\langle n_{10}\rangle$") +axes[2, 1].set(title="(f) Surviving excitation", ylabel=r"$N=\sum_i\langle n_i\rangle$") +axes[2, 1].legend(fontsize=7, loc="lower left") plt.show() ``` -This simulation combines continuous Hamiltonian evolution and instantaneous -digital operations in one digital–analog program. In this simple application, -the digital midpoint pulse reverses the coherent phase accumulated during the -analog evolution, a phase-cancellation technique known as a Hahn echo. With -Markovian dephasing, the coherent phase is still cancelled, but the pulse cannot -recover lost signal contrast. - -## Useful things to know - -- Segments run in the order they appear in `SimulationProgram`. -- Analog-only Hamiltonian quenches belong in {doc}`hamiltonians` - (`Hamiltonian.piecewise`). Use a program when analog evolution sits in a - protocol with digital gates. -- Observables, `random_seed`, and `get_state` are program-wide; leave them unset - on segment params. `num_traj` may be unanimous on segments or set on the - program. `shots` stay on digital segment params. -- YAQS passes the state between segments automatically and does not mutate the - input state you give `Simulator.run`. -- A noise model passed to `Simulator.run` is inherited by every segment unless - that segment supplies a third-tuple override (`None` inherits; an empty - `NoiseModel()` disables noise). -- Noisy trajectories and their RNG streams remain continuous across segment - boundaries. -- A noiseless program can retain its final state with - `SimulationProgram(..., get_state=True)`. -- Digital segments may operate on qubit sites in a heterogeneous state while - non-qubit sites remain spectators. Digital gates themselves currently require - qubit targets. -- Independent trajectories may run in parallel with `Simulator(parallel=True)`. - -## Related topics - -- {doc}`hamiltonians` — analog quench with `Hamiltonian.piecewise` -- {doc}`analog_simulation` — standalone noisy analog evolution -- {doc}`circuit_observables` — standalone digital circuit simulation and - OpenQASM -- {doc}`scheduled_jumps` — deterministic jumps on an analog time grid -- {doc}`simulation_parameters` — simulation accuracy and output controls +**Loss and dephasing limit the return in different ways.** Relaxation reduces +the total population, while the dephased excitation survives in a broader +spatial distribution. Increasing dephasing weakens the echo over the rates +shown. The lower panels separate the occupation returning to site 10 from the +total excitation remaining in the chain. Shading shows one standard error +estimated from complete trajectories; the dotted line gives the exact relaxation +envelope. These bands describe sampling uncertainty, not timestep or MPS +truncation error. Increase `num_traj` to reduce sampling fluctuations, and check +numerical convergence before interpreting small differences. + +## Further options + +Programs require an MPS initial state. Segment parameters control local timing, +accuracy, gate mode, and digital `shots`. Observables, `random_seed`, and +`get_state` belong on the program. A noiseless program can retain its final +state with `get_state=True`; noisy programs do not return a single final MPS. A +program-level `num_traj` overrides segment budgets. If omitted, all segment +budgets must agree. Digital segments can target qubits in a heterogeneous MPS; +non-qubit sites remain spectators. For analog-only Hamiltonian changes, use +`Hamiltonian.piecewise` as described in {doc}`hamiltonians`. + +An optional third tuple entry overrides noise for one segment, for example +`(hamiltonian, analog_params, local_noise)`. `None` inherits run-level noise; an +empty `NoiseModel()` disables it for that segment. Digital operators may also be +OpenQASM strings or paths. Pass the pair list directly to `Simulator.run` with +program-wide keywords when a named program is unnecessary. The outer `counts` +contains the histogram from the last segment that sampled shots; inspect +`segment_results` for earlier histograms. Program execution does not support +`multi_time_observables`. + +For deterministic scheduled jumps, see {doc}`scheduled_jumps`. Jump times use +the analog run's local clock and must follow its `dt` grid with `order=1`. +Consecutive compatible analog segments share that clock; a digital gate starts a +new analog run. Use a segment noise override to attach a schedule to one +interval. For device-specific noise strengths and distributions, see +{doc}`realistic_noise_models`. From 82b73856d92d5d7d170c4ea95fbe9f72ce588621 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:09:58 +0200 Subject: [PATCH 13/30] added analog-digital to quickstart --- docs/examples/quickstart.md | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/docs/examples/quickstart.md b/docs/examples/quickstart.md index 82ccfb359..32e0353ba 100644 --- a/docs/examples/quickstart.md +++ b/docs/examples/quickstart.md @@ -168,6 +168,85 @@ Damping shifts the readout toward fewer excitations. Each histogram summarizes 256 shots. See {doc}`circuit_shots` for bitstring counts and {doc}`circuit_observables` for expectation values and OpenQASM input. +## Noisy Analog-Digital Simulation + +Can digital gates reverse the spreading in an analog spin chain? Prepare one +excitation in a **20-site XY chain**, then apply $Z$ gates on even sites halfway +through the evolution. Compare free evolution, refocusing, and refocusing with +dephasing. + +```{code-cell} python +from qiskit import QuantumCircuit + +from mqt.yaqs import AnalogSimParams, DigitalSimParams, Hamiltonian, NoiseModel, Observable, SimulationProgram, Simulator, State + +length = 20 +center = length // 2 +state = State(length, initial="zeros") +hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) +observables = [Observable("z", site) for site in range(length)] +preparation = QuantumCircuit(length) +preparation.x(center) +phase_pulse = QuantumCircuit(length) +phase_pulse.z(range(0, length, 2)) + +analog_params = AnalogSimParams(elapsed_time=1.5, dt=0.25, order=2, preset="fast") +digital_params = DigitalSimParams(preset="fast") +free_program = SimulationProgram( + [(preparation, digital_params), (hamiltonian, analog_params), (hamiltonian, analog_params)], + observables=observables, num_traj=32, random_seed=7, +) +echo_program = SimulationProgram( + [(preparation, digital_params), (hamiltonian, analog_params), + (phase_pulse, digital_params), (hamiltonian, analog_params), (phase_pulse, digital_params)], + observables=observables, num_traj=32, random_seed=7, +) +dephasing_rate = 0.2 +noise = NoiseModel([ + {"name": "pauli_z", "sites": [site], "strength": dephasing_rate} for site in range(length) +]) + +simulator = Simulator(show_progress=False) +free_result = simulator.run(state, free_program) +echo_result = simulator.run(state, echo_program) +noisy_echo_result = simulator.run(state, echo_program, noise_model=noise) +``` + +```{code-cell} python +:tags: [hide-input] +from matplotlib.colors import PowerNorm + +fig, axes = plt.subplots(1, 3, figsize=(7.2, 2.8), sharex=True, sharey=True) +for index, (ax, result, title) in enumerate(zip( + axes, (free_result, echo_result, noisy_echo_result), + ("(a) Free evolution", "(b) Refocusing", "(c) Noisy refocusing"), strict=True, +)): + segments = [segment for segment in result.segment_results if segment.segment_type == "analog"] + times = np.concatenate([segment.times + segment.time_offset for segment in segments]) + values = np.concatenate([np.asarray(segment.expectation_values) for segment in segments], axis=1) + keep = np.r_[True, np.diff(times) > 0] + occupation = (1 - values[:, keep]) / 2 + image = ax.pcolormesh( + times[keep], np.arange(length), occupation, shading="auto", cmap="cividis", + norm=PowerNorm(0.5, vmin=0, vmax=1), rasterized=True, + ) + if index > 0: + ax.axvline(1.5, color="white", linestyle="--", linewidth=1) + ax.set(xlabel=r"Time $t$", xlim=(0, 3), yticks=[0, 5, 10, 15, 19]) + ax.set_title(title, loc="left", fontsize=10) +axes[0].set_ylabel(r"Site $i$") +fig.colorbar(image, ax=list(axes), label=r"$\langle n_i\rangle$", ticks=[0, 0.1, 0.5, 1]) +plt.show() +``` + +The phase pulse reverses the XY exchange, bringing the excitation back at $t=3$. +A final pulse restores the phase frame. Dephasing during the analog intervals +preserves excitation but disrupts refocusing; the noisy panel averages 32 +trajectories at Lindblad rate $\gamma_z=0.2$. The gates are ideal and +instantaneous. All panels share one color scale. See +{doc}`digital_analog_simulation` for the pulse mechanism, program outputs, and +noise comparisons. + ## Circuit equivalence Verify a transpiled circuit, then compare the effects of an added rotation and From 7ccc5c90be49dfa624da96478434b3b751428728 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:12:55 +0200 Subject: [PATCH 14/30] updated index names --- DOCS_TODO.md | 737 +++++++++++++++++++++++++++++++++++++++++++++++ PAPER_WRITING.md | 354 +++++++++++++++++++++++ TODO.md | 439 ++++++++++++++++++++++++++++ docs/index.md | 14 +- 4 files changed, 1537 insertions(+), 7 deletions(-) create mode 100644 DOCS_TODO.md create mode 100644 PAPER_WRITING.md create mode 100644 TODO.md diff --git a/DOCS_TODO.md b/DOCS_TODO.md new file mode 100644 index 000000000..0126150ac --- /dev/null +++ b/DOCS_TODO.md @@ -0,0 +1,737 @@ +# YAQS 1.0 Documentation TODO + +## Goal and scope + +Make the documentation a concise guide to the workflows and options available in +YAQS. Restore reliable Read the Docs builds within the 15-minute limit. Document +the existing feature set; add no library features for this work. + +Work through the chunks in small, reviewable batches. Use the workflow-guide +order below for the detailed examples. Preserve supported workflows, scientific +interpretation, and existing page URLs. Keep detailed method explanations and +implementation details in advanced sections or the API reference. Leave +template-managed files unchanged; contribute required template fixes upstream. + +## Build baseline + +The review on 2026-10-08 covered documentation at `8684385e` and existing local +notebook caches. + +- [Read the Docs build 35013130](https://app.readthedocs.org/projects/mqt-yaqs/builds/35013130/) + at `d6e8add6` spent 116 seconds installing dependencies and reached the + 900-second timeout during Sphinx. The unfinished Sphinx command did not retain + per-notebook timings. +- The baseline documentation install included all extras, the CUDA-enabled Torch + stack, and development dependencies. +- `sphinx_llm.txt` started a second Sphinx build for Markdown. HTML and Markdown + used separate notebook caches. Both caches contain executions of identical + example code. +- An isolated HTML render at `8684385e`, with notebook execution and Markdown + generation disabled, took 39 seconds locally. Strict checks failed on + documentation warnings; this was not a successful full documentation build. + +Historical local cache timings, mostly from Python 3.12: + +| Notebook | HTML execution | Markdown execution | +| ---------------------- | -------------: | -----------------: | +| Quickstart | 176 s | 173 s | +| Noise characterization | 27 s | 29 s | +| Memory surrogate | 15 s | 17 s | +| Environmental memory | 13 s | 14 s | +| All 19 notebooks | 322 s | 323 s | + +These are execution totals from separate historical runs, not elapsed time for +one build or current RTD timings. Code hashes match 17 of the 19 current +notebooks, but library versions and execution environments have changed. Fresh +profiling must establish the remaining bottlenecks. + +## Chunk 1: Restore a fast documentation build + +- [x] Make Markdown generation reuse the HTML build's parsed notebooks and + outputs. Start with `llms_txt_build_parallel=False` and an explicit shared + `nb_execution_cache_path` in `docs/conf.py`. Preserve the MQT LLM files, + including an explicit `llms_txt_full_build=True`. +- [x] Align local Nox and RTD documentation installation. Omit development and + test dependencies, use CPU-only Torch, and retain dependencies needed by + the OpenQASM 3 and surrogate examples. +- [x] Bound numerical threads and process workers in the documentation build + environment. Preserve ordinary examples' default parallel execution and + suppress progress bars in rendered documentation. +- [x] Measure a cold build. Record the commit, environment, dependency-install + time, per-notebook execution time, HTML and Markdown generation time, and + total build time. Confirm that each notebook executes only once. +- [x] Reduce example workloads only where fresh timings justify it. Preserve + meaningful outputs and coverage of supported optional paths. Keep + expensive numerical validation in the appropriate test or + release-validation tier. The local cold build met the target without + reducing workloads. +- [x] Remove `htmlzip` if the downloadable archive is not needed. + +Acceptance: a cold RTD build completes in roughly 10 minutes or less, leaving +headroom below the 15-minute limit. Required examples and optional paths remain +validated. Cached builds alone do not satisfy this check. + +### Chunk 1 validation: 2026-10-08 + +The final local cold build used `8684385e` plus the configuration changes above, +Python 3.14.2, a fresh environment, and empty uv, notebook, Numba, and Sphinx +caches. The build completed in **9 minutes 50 seconds**. + +| Phase | Time | +| ---------------------------------- | ----: | +| Dependency installation | 200 s | +| HTML parsing, execution, rendering | 372 s | +| Markdown generation | 3 s | +| LLM file combination and shutdown | 15 s | +| Total, including environment setup | 590 s | + +Notebook execution took 335 seconds within the HTML phase. All 19 notebooks +executed once; Markdown executed none. The slowest guides were circuit +observables (75 s), noise characterization (44 s), and analog simulation (41 s). +No workload reductions were needed. The build retained 33 SVG plot outputs, +per-page Markdown, `llms.txt`, and `llms-full.txt`. No notebook produced an +execution error or progress bar. + +The environment contained CPU-only Torch and the OpenQASM 3 importer, with no +CUDA, Triton, development, or test packages. The strict integration test passed +with these resolved documentation dependencies. Full lint and the new-file hooks +passed. + +The full build did not use `-W`. The parent build reported 1,443 warnings and +the Markdown child reported 1,204 warnings; chunk 4 must resolve these. Cold RTD +validation remains pending after publication of the changes. + +Local evidence, including source hashes, package versions, full notebook +timings, logs, and generated pages: +[/tmp/yaqs-docs-chunk1-final-8684385e/report.md](/tmp/yaqs-docs-chunk1-final-8684385e/report.md). + +## Chunk 2: Shorten first-use and configuration guides + +- [x] Keep `quickstart.md` as a compact tour of the main use cases. Showcase + useful scales within the documentation budget, with scientifically + meaningful plots and a consistent journal style. Keep analog, digital, + analog-digital, equivalence, memory, noise fitting, and surrogate + examples. Fold plotting code, link to dedicated guides, and omit + unnecessary advanced settings. +- [ ] Simplify `simulation_parameters.md`: one preset table, one override + example, and short analog and digital recipes. Explain when to change a + setting and what the change affects. Remove repeated override rules and + move gate-update mechanics into an advanced section. +- [ ] Simplify `simulator_initialization.md`: common controls and one reusable + example first. Move CPU-discovery details, process internals, and retry + customization into an advanced section. Move the result catalogue into the + results guide in chunk 3. +- [ ] Start `custom_gates.md` with the common task of supplying a custom + unitary. Put DAG translation, `BaseGate` fields, manual gate construction, + and generator details later in the page. +- [ ] Start `hamiltonians.md` with the built-in model catalogue and construction + examples. Move energy and correlation contractions after model selection. +- [ ] Consolidate backend-selection explanations. Use one main representation + guide and link to it from state, Hamiltonian, and workflow pages. +- [ ] Extend the quickstart examples into the dedicated workflow guides using + the checklist below. Replace competing introductory examples with one main + worked example per guide. Preserve distinct supported workflows in focused + later sections or linked advanced guides. +- [ ] Preserve units, time grids, spatial ordering, shot and trajectory budgets, + accuracy tradeoffs, supported restrictions, and the meaning of scientific + diagnostics. Move deeper explanations rather than remove needed context. + +Acceptance: each guide leads with its purpose, a small working example, the main +user choices, and how to read the output. Detailed signatures remain in the API +reference. All existing capabilities remain discoverable. + +### Workflow guides: extend the quickstart examples + +Keep the quickstart as a compact tour. Each detailed guide should stand alone, +use the corresponding quickstart model and terminology, and explain each step in +the order a user performs it. Work through one guide at a time in the order +below. Extend the scientific question and user choices before increasing system +size, trajectories, sweeps, or training cost. + +Follow [PAPER_WRITING.md](PAPER_WRITING.md) for prose and structure. Begin with +the physical question, narrow to the setup and technical steps, then return to +what the results mean and where the conclusion stops. Connect paragraphs through +the questions the reader needs answered. Keep the instructional steps, but avoid +clipped prose, filler, and repeated claims. + +Shared structure for each guide: + +1. State the task, expected result, and prerequisites, including optional + extras. +2. Build the model, initial state, and noise or controls. Explain their physical + meaning, units, site ordering, and the supported input forms used here. +3. Choose accuracy and sampling settings. Explain the few settings that matter + for this task and link to the setup guides for the full options. +4. Initialize the public interface, run the calculation, and extract the output. + Explain the relevant result fields, array axes, and ordering beside the code. +5. Read the figure, add one useful extension or reference check, and explain the + limits of the conclusion. End with a short options summary and related + guides. + +Keep working code visible and plotting code folded. Use the quickstart's journal +figure style, clear panel labels, units, and shared scales for comparisons. +Explain decisions and interpretation; keep algorithm derivations, full +signatures, and internal helpers in advanced sections or the API reference. +Preserve default parallel execution, show separate initialization and run calls, +and suppress progress only for the documentation. Explain the script entry-point +guard where relevant. Use supported public imports. + +Where supported, extend each walkthrough with a small noise-strength comparison: +include a noiseless baseline and several clearly different strengths, such as +weak, intermediate, and strong noise. Keep the Hamiltonian or circuit, initial +state, time grid, and accuracy settings fixed. Use comparable sampling budgets, +label rates or probabilities correctly, and explain sampling uncertainty. Use +shared axes and color scales so each figure shows the physical change rather +than a change in normalization. Choose strengths after checking that the effect +is visible and the runtime is reasonable. Distinguish added Markovian noise from +the effects of coupling to an explicit environment. + +#### 1. Analog simulation — `docs/examples/analog_simulation.md` + +- [x] Expand the 20-site excitation-transport example: construct the XY + Hamiltonian, prepare the localized excitation, define relaxation, select + observables and time grid, and run noiseless and noisy simulations. +- [x] Explain occupation heatmaps and total excitation, including boundary + reflections, relaxation, and trajectory fluctuations. Keep the analytic + decay comparison and distinguish an ensemble expectation from a finite + trajectory estimate. +- [x] Finish with a row or grid of occupation heatmaps for zero, weak, + intermediate, and strong relaxation, with shared time and site axes and + one color scale. Reuse the baseline runs. Compare total excitation on a + companion plot and explain which transport features relaxation suppresses. + Explain how to choose trajectory count, accuracy preset, and system size. + Preserve distinct existing capabilities through later sections or links. + +The comparison uses relaxation rates 0, 0.5, 1.5, and 4 with one shared color +scale. The nonzero rates span lifetimes from 2 to 1/4, giving visible +differences in how much excitation survives during transport. The guide follows +the physical question through the setup to the interpretation and limits of the +result. + +Validation on 2026-10-09: all nine guide cells executed in 67 seconds in the +existing Python 3.12 documentation environment with capped threads and two +workers. The noiseless/strong-relaxation pair took 22 seconds; the additional +weak/intermediate runs took 43 seconds. Independent single-excitation dynamics +gave a maximum coherent occupation error of 0.0051. Checks passed for result +axes, trajectory averages, excitation conservation, conditional spatial +profiles, and analytic decay within finite-sample uncertainty at all four rates. + +The isolated strict HTML build passed, and both figures were inspected. Plotting +cells were folded, Markdown reused execution, and LLM files remained available. +Two SVG MIME-priority warnings remain in the Markdown child for chunk 4. Linked +guide paths were checked but their contents were stubbed in this isolated build; +full-site and final cold-build validation remain pending. + +Preview, raw trajectory data, CSV references, figures, timings, and logs: +[/tmp/yaqs-analog-guide/report.md](/tmp/yaqs-analog-guide/report.md). + +#### 2. Circuit measurements — `docs/examples/circuit_shots.md` + +- [x] Expand the 16-qubit circuit example: build the circuit, prepare the input, + define damping, set shots and accuracy, initialize the simulator, and + collect noiseless and noisy counts. +- [x] Explain outcome encoding and how counts become probabilities and grouped + excitation-number histograms. Distinguish shots from trajectories and + explain finite-sample fluctuations without promising identical histograms. +- [x] Compare grouped readout histograms at zero and several damping strengths, + keeping the circuit and shot budget fixed. Explain the shift in excitation + number and finite-sample fluctuations. Keep individual outcomes + discoverable alongside the grouped histogram. Base + `circuit_observables.md` on the analog XY transport example, reconstruct + its dynamics with exchange gates and mid-circuit observable checkpoints, + and compare noiseless and noisy results. Explain how circuit noise + strengths relate to the represented time step. Retain OpenQASM inputs and + gate-application choices in focused later sections. + +The shot guide uses a 16-qubit graph state and rates 0, 0.1, 0.5, and 1.5, +comparing complete excitation-number histograms with a shared sampled baseline. +The observable guide uses the analog example's 20-site XY chain, localized +excitation, time grid, and rates 0, 0.5, 1.5, and 4. Symmetric Trotter steps +reconstruct the transport heatmaps. Per-site gate counts set noise strengths so +each step accumulates the intended relaxation exposure. A companion figure +compares analog and digital profiles, Trotter-step refinement, and excitation +survival. Sixteen noisy trajectories keep the observable example within the +documentation budget. Plotting code is folded. + +Validation on 2026-10-09 used the existing Python 3.12 documentation environment +with capped threads and two workers. The six shot-guide cells took 77 seconds; +checks covered Qiskit probabilities, the exact noiseless binomial distribution, +weak-noise marginals within sampling uncertainty, bit encoding, and count +totals. The nine observable-guide cells took 104 seconds. Against independent +exact single-excitation dynamics, the maximum coherent occupation error fell +from 0.0070 to 0.0017 when the circuit step halved. The analog reference error +was 0.0051. Noisy outputs matched an independent finite-circuit channel +reference within sampling uncertainty, with conditional spatial-profile errors +below 0.000001. Trajectory aggregation, checkpoint axes, excitation conservation +or loss, OpenQASM counts, and both gate modes also passed checks. + +The isolated strict HTML builds passed and all three figures were inspected. +Markdown reused execution, and LLM files remained available. SVG MIME-priority +warnings remain in the Markdown children for chunk 4. Linked guide paths were +checked but their contents were stubbed; full-site and final cold-build +validation remain pending. + +Previews, raw data, CSV files, figures, timings, and logs: + +- Shots: + [/tmp/yaqs-circuit-guides/report.md](/tmp/yaqs-circuit-guides/report.md). +- Observables: + [/tmp/yaqs-digital-xy-guide/report.md](/tmp/yaqs-digital-xy-guide/report.md). + +#### 3. Circuit verification — `docs/examples/equivalence_checking.md` + +- [x] Expand the quickstart comparison into hardware-constrained compilation, a + deliberate rotation-angle bug, and a noise-strength sweep. Rename the + guide and sidebar entry to Circuit Verification. +- [x] Explain checker setup, output-layout alignment, returned overlap, decision + thresholds, and sampling uncertainty. Compare correct and faulty + compilations across noiseless, weak, and stronger Pauli noise on shared + axes. Distinguish the offline target and assumed noise from measured + device data. +- [x] Keep backend selection, OpenQASM inputs, and execution controls as short + option sections after the worked example. Validate against independent + unitary and noisy-channel references and remove unsupported performance + claims. + +The guide compiles a four-qubit circuit for a nearest-neighbor target using +native `rz`, `sx`, `x`, and `cx` gates. Routing increases the controlled-X count +from three to nine. The reference includes the final output permutation; without +this alignment the overlap is 0.25, while the aligned comparison gives one. The +compiled circuit keeps its physical gate sequence for the noise sweep. A native +rotation-angle error follows the exact cosine overlap. Correct and faulty +compilations then use six Pauli-error probabilities and 256 trajectories per +point. Plotting code is folded, and default backend selection and parallel +execution remain enabled. + +Validation on 2026-10-09 used the existing Python 3.12 documentation environment +with capped numerical threads and two workers. All five code cells executed in +about 10 seconds, including independent checks. Qiskit's layout-aware unitary +verified the compilation; dense operators checked every angle point. Exact +Qiskit channels summed the Pauli branches at every eligible gate for both +implementations. Sampled fidelities differed from these references by less than +1.6 standard errors. Additional checks covered native connectivity, retained +trajectory aggregation and error estimates, reproducibility across worker +counts, and agreement between matrix and MPO backends. + +The isolated strict HTML build passed and the figure was inspected. Markdown +reused execution and LLM files remained available. SVG MIME-priority and +bibliography-node warnings remain in the Markdown child for chunk 4. Linked +guide paths were checked but their contents were stubbed; full-site and final +cold-build validation remain pending. + +Preview, raw data, exact references, CSV files, figure exports, timings, and +logs: +[/tmp/yaqs-verification-guide/report.md](/tmp/yaqs-verification-guide/report.md). + +#### 4. Environmental memory — `docs/examples/characterization.md` + +- [x] Expand the coupling sweep: define the system and environment, configure + the probing schedule and cut, run characterization, and extract spectra + and entropy from the results. +- [x] Explain normalized spectral weights, entropy, and what they reveal about + the response to the chosen probes. Distinguish these diagnostics from + environment populations and mixed-state Schmidt spectra. Explain the + nonmonotonic coupling result without claiming a universal memory measure. +- [x] Explain the main sampling and intervention choices. Keep conditioned reset + delay, response-mode inspection, and process-tensor diagnostics as focused + sections, preserving the `memory-theory` and `reset-delay` anchors. +- [x] Add a small dephasing comparison through dense process-tensor tomography, + then characterize the reconstructed tensors. Explain that the direct + Hamiltonian path does not accept a `NoiseModel`, and distinguish coupling + changes from added Markovian noise. Validate the reconstruction and limit + claims about small noisy spectral weights. + +The main example uses three Ising spins, with site 0 as the probe and two spins +as its environment. Thirteen couplings share one 8-by-8 probe grid, four +interventions, and cut 2. The guide explains the five evolution intervals, +outcome-probability weighting, response-matrix axes, retained singular values, +entropy, and effective modes. A red spectrum sweep and entropy plot reproduce +the quickstart's nonmonotonic result. Seven explicit reset delays show +conditioned persistence without implying an all-outcome memory length. + +The added-noise example uses two spins and one intervention to keep dense +tomography small. Each noisy reconstruction uses 16 sequences and 512 +trajectories per sequence, with integration step 0.025. Dephasing rates 0, 1, +and 4 concentrate the response in the leading mode. A separate temporal-entropy +calculation explains its distinction from probe-response entropy. Public +imports, automatic representation selection, and default parallel execution +remain in the examples. Plotting code is folded. + +Validation on 2026-10-09 used the existing Python 3.12 documentation environment +with capped numerical threads and two workers. All nine cells executed in 45 +seconds, including independent checks. Explicit spin-Hamiltonian exponentials +and selected-branch density-matrix evolution reproduced every response-matrix +entry in the coupling and delay sweeps within 0.00000005. Checks also covered +probe reuse, SVD reconstruction, tail weight, entropy, effective modes, and the +uncoupled rank-one limit. + +An independent continuous-time Lindblad generator reproduced the qualitative +noise effect. The noisy response matrices differed from this reference by at +most 0.022 and 0.031, including finite-step and sampling error. The strongest +noise's small entropy remains sensitive to the sampling floor; the guide does +not interpret every retained tail mode as physical memory. Reconstructed tensors +passed explicit Hermiticity, positivity, and causal-normalization checks. +Noiseless dense and uncapped direct-MPO tensors agreed, including response +matrices and temporal entropy. + +The isolated strict HTML build passed and all three figures were inspected. +Markdown reused execution and LLM files remained available. Three SVG +MIME-priority warnings remain in the Markdown child for chunk 4. Linked guide +paths were checked but their contents were stubbed; full-site and final +cold-build validation remain pending. + +Preview, raw matrices, exact references, CSV files, figure exports, timings, and +logs: [/tmp/yaqs-memory-guide/report.md](/tmp/yaqs-memory-guide/report.md). + +#### 5. Noise characterization — `docs/examples/digital_twin.md` + +- [x] Expand the four-site transport example: generate synthetic dynamics, + select endpoint observations, define candidate relaxation and dephasing + channels, choose initial guesses and bounds, and fit the rates. +- [x] Explain observation axes, time alignment, observable selection, and + parameter order and meaning. State that channel types and locations are + assumed known; fitting strengths does not discover an arbitrary model. +- [x] Rerun the fitted model, compare dynamics on shared heatmap scales, and + validate withheld interior observables. Explain how measured data and + sampling uncertainty replace synthetic input. Keep stochastic fitting and + optimizer controls as short options after the worked example. +- [x] Show zero, weaker, reference, and stronger noise with matched occupation + heatmaps. Fit only the reference case and reuse its fitted dynamics for + validation. Explain the distinct physical effects of relaxation and + dephasing without attributing their combined sweep to one channel alone. + +The guide fits two local Lindblad rates in a four-spin XY chain from endpoint Z +traces. It explains the initial excitation, observation grid, jump operators, +rate bounds, mean-squared objective, and fitted result. Two figures show the +noise-strength comparison, reference and fitted transport, and predictions at +the withheld interior sites. All heatmaps share a square-root color scale. Only +one optimization runs. Public imports, automatic fitting-backend selection, and +default parallel settings remain. Plotting code is folded. + +Measured-data guidance covers observable and time ordering, converting +occupations to Z expectations, equal objective weights, finite-shot uncertainty, +model assumptions, and limits on identifiability and extrapolation. +Forward-model and optimizer options explain stochastic sampling, random seeds, +scalar search, and result fields without further fits or promises of future +features. + +Validation on 2026-10-09 used the existing Python 3.12 documentation environment +with capped numerical threads and two workers. All eight cells executed in 14 +seconds, including independent checks; the fit took nine seconds. Fitted rates +were 0.3499 and 0.1199 for synthetic rates 0.35 and 0.12. Endpoint Z-trace RMSE +fell from 0.069 to 0.00011, and withheld interior RMSE was 0.000071. + +An independent five-state vacuum-plus-single-excitation master equation, +integrated with SciPy DOP853, reproduced every site and time sample at all four +noise scales and for the fitted rerun within 0.000000000012. Checks also covered +trace, positivity, excitation conservation or loss, initial-state placement, +observation ordering, fit versus rerun consistency, process ordering, unchanged +initial guesses, and optimizer losses. Separate channel references confirmed +that dephasing preserves population while changing transport. A local endpoint +sensitivity check found distinct rate signatures; this does not establish global +identifiability or experimental confidence intervals. + +The isolated strict HTML build passed and both figures were inspected. Markdown +reused execution and LLM files remained available. Two SVG MIME-priority +warnings remain in the Markdown child for chunk 4. Linked guide paths were +checked but their contents were stubbed; full-site and final cold-build +validation remain pending. + +Preview, raw dynamics, independent references, CSV files, figure exports, loss +history, timings, and logs: +[/tmp/yaqs-noise-guide/report.md](/tmp/yaqs-noise-guide/report.md). + +#### 6. Experimental surrogate models — `docs/examples/memory_surrogate.md` + +- [x] Expand the random-control training and unseen pulse-angle sweep through + public `MemoryCharacterizer.sample`, `train`, and `predict` calls. Explain + the environment preparation, intervention schedule, training set, + checkpoint-selection validation set, and chosen prediction sequences. +- [x] Explain the final-state Bloch-plane plot and coherence comparison with + free evolution. Add an independent small-system reference comparison; + report prediction errors and check trace, Hermiticity, and positivity + without concealing errors through clipping or projection. +- [x] Keep the experimental and publication-status note. Explain that random + validation accuracy does not certify chosen controls or longer horizons. + Keep this example within the validated two-intervention horizon; reliable + long-protocol generalization is separate work, not a documentation + promise. +- [x] If training and validation cost permit, compare the control response at + weak and stronger system-environment coupling. Train and validate a + separate model for each Hamiltonian; the current model does not take + coupling strength as a prediction input. The public training path does not + accept a `NoiseModel`, so describe this as an environmental-coupling + comparison. Keep added-noise training outside the documentation scope. + +The guide now trains separate models at $J=0.3$ and $J=1$ using 4,096 random +unitary sequences and 256 checkpoint-selection sequences per Hamiltonian. The +schedule contains two interventions and two evolution intervals of 0.6. Both +models predict the same 61-angle pulse sweep from a probe in $|+\rangle$ and an +environment in $|0\rangle$. The worked example uses only public YAQS imports, +retains default parallel data generation, and suppresses documentation progress +bars. It replaces the repeated zero-evolution training examples with one +training loop, a control-response plot, and a coupling/reference comparison. + +All 7 production code cells matched the executed notebook. In the existing +Python 3.12 documentation environment, capped to one numerical thread and two +workers, execution took 171.5 seconds including independent checks; the +two-model sampling and training cell took 167.2 seconds. The direct reference +builds the Hamiltonian from Pauli matrices and evolves the joint state with +SciPy's matrix exponential. An explicit index sum independently checks the +partial trace. Checks also cover site ordering, schedule, both intervention +outputs, periodic pulse endpoints, array shapes, finite values, and unchanged +probe preparation. + +For $J=0.3$ and $J=1$, coherence RMSE was 0.0179 and 0.0339, respectively. The +maximum half-trace-norm matrix errors were 0.0513 and 0.0593. All returned +matrices had positive eigenvalues in these sweeps, with maximum trace errors +0.0062 and 0.0092. The facade makes estimates Hermitian; it does not enforce +normalization or positivity. The guide checks and reports these properties +without clipping, renormalization, or projection. It retains the experimental +and unpublished status, the fixed environment and schedule, and the limits of +generalization beyond the tested unitary controls and horizon. + +The isolated strict HTML build passed and both SVG figures were inspected. +Markdown reused notebook execution and LLM files remained available. The 2 SVG +MIME-priority warnings in the Markdown child remain tracked for chunk 4. The +two-model comparison adds training cost; its place in the total 15-minute budget +must be checked in the final full-site cold build. These isolated runs use +existing dependencies and compilation caches, and linked page contents were +stubbed. They do not establish cold RTD build time. + +Preview, raw predictions, independent reference states, CSV data, trained state +dictionaries, figure exports, timings, and logs: +[/tmp/yaqs-surrogate-guide/report.md](/tmp/yaqs-surrogate-guide/report.md). + +#### 7. Analog-digital simulation — `docs/examples/digital_analog_simulation.md` + +- [x] Replace the one-qubit example with a 20-site XY excitation echo. Prepare + the excitation with a circuit, alternate analog intervals and staggered + phase pulses, and explain how the final pulse restores the phase frame. +- [x] Explain program-wide settings, local segment outputs, instantaneous gate + timestamps, and occupation extraction across repeated analog boundaries. + Retain supported program options in a short final section. +- [x] Compare the noiseless echo with uniform relaxation at rate 0.5 and local + Pauli-Z dephasing at rates 0.05 and 0.2. Use shared heatmap scales and + pointwise standard errors. Explain the difference between excitation loss + and loss of refocusing without equating the channel strengths. +- [x] Execute all cells, inspect the figures, and compare the plotted dynamics + with an independent Lindblad reference. Keep the final full-site and cold + RTD build checks pending. + +All 7 production cells matched the executed notebook. Execution took 79.3 +seconds in the existing Python 3.12 documentation environment, with one +numerical thread and two workers. The three noisy simulations took 75.2 seconds +together. The independent reference constructs the nearest-neighbor hopping +matrix and Lindblad generator in the vacuum and single-excitation sector, then +applies the phase pulses explicitly. It uses no YAQS propagation or Hamiltonian +helpers. + +The noiseless return was 0.99994, against the exact value 1. The maximum +occupation error over all coherent site/time samples was 0.0051. With +relaxation, the final population was 0.21875 against the exact value 0.22313. +The two dephasing returns were 0.650 and 0.341, against reference values 0.719 +and 0.350; their estimated standard errors were 0.068 and 0.059. All noisy +profiles passed the recorded finite-ensemble bounds. Checks also covered +input-state preservation, observable ordering, trajectory means, segment +continuity, timelines, and population conservation under dephasing. Population +uncertainty was calculated after summing sites within each trajectory. + +The isolated strict HTML build passed and both SVG figures were inspected. +Markdown reused execution and LLM files remained available. Three MIME-priority +warnings remain in the Markdown child: two figure outputs and one text output. +These remain tracked for chunk 4. Linked page paths were checked, but their +contents were stubbed. The run used existing dependencies and warm compilation +caches, so it does not establish full-site or cold RTD build time. + +Preview, raw trajectories, independent references, CSV data, figure exports, +source hash, versions, timings, and logs: +[/tmp/yaqs-hybrid-guide/report.md](/tmp/yaqs-hybrid-guide/report.md). + +#### Preserve other workflows and validate each guide + +- [ ] Keep analog-digital programs, custom gates, hardware models, scheduled + jumps, and ensembles discoverable. Reuse setup and terminology where + useful; retain their distinct worked examples rather than force them into + a quickstart example that does not cover their purpose. +- [ ] Execute each revised guide independently and inspect its figures. Check + public imports, result interpretation, meaningful numerical references, + links, and lint before Aaron reviews the guide. +- [ ] Record per-guide execution time and cumulative cold-build cost. Quickstart + and detailed notebooks execute separately; matching code snippets alone do + not share execution. Reuse expensive fits and trained models within each + guide, and avoid extra runs merely to produce another plot. +- [ ] Remove superseded introductory examples and repeated option catalogues + only after checking that all distinct supported capabilities remain + documented. Update quickstart links and the homepage task table as needed. + +Acceptance: each main quickstart example has a clear, independently runnable +walkthrough with an explained extension or validation. Include a meaningful +noise-strength comparison wherever the public workflow and build budget allow +it; document the supported alternative where they do not. The guide teaches +users how to adapt the workflow, retains its scientific limits, and fits the +final cold-build budget. Complete the full-site validation in chunk 4 after the +guide reviews. + +### Quickstart validation: 2026-10-08 + +The six workflows executed in 136 seconds in the existing Python 3.12 +documentation environment. Noiseless and noisy transport on 20 sites took 23 +seconds; 16-qubit readout took 26 seconds; endpoint noise fitting and its +simulation rerun took 9 seconds; surrogate training and its pulse-angle sweep +took 76 seconds. Equivalence and memory sweeps together took 3 seconds. The +isolated strict HTML build took 142 seconds. + +Numerical checks passed for excitation conservation, circuit sampling and its +damping shift, analytic equivalence overlaps, memory spectra, and fitted noise +rates. Earlier independent transport and withheld-site noise checks are in the +comparison report linked below; those five example workflows are unchanged. + +The surrogate uses only public `MemoryCharacterizer.sample`, `train`, and +`predict` calls. It trains on 4,096 random unitary sequences and selects a +checkpoint using 256 random validation sequences. One model predicts 61 chosen +Z-pulse angles, applied between two evolution intervals. The figure shows final +coherence against pulse angle and the predicted free-evolution baseline. + +Private matrix-exponential references give coherence RMSE 0.034 and maximum +error 0.078 across the sweep. Complex density-matrix entry RMSE is 0.032. The +predictions are unmodified, Hermitian, and positive in this example, with trace +errors below 0.005. The zero and full-turn pulses give the same prediction. The +smaller training budget failed fresh sweep checks. This validates the shown +short-horizon coherence sweep, not arbitrary controls, long horizons, or exact +density-matrix reconstruction. Independently check the surrogate guide's +chosen-control examples during its review. + +All 15 production code cells matched the executed notebook. Six SVG figures +rendered, plotting and training cells were folded, and LLM files remained +available. The surrogate figure shows final probe states in the Bloch plane, +colored by pulse angle, beside a coherence sweep with shaded gains and losses. A +section note marks surrogate modeling as experimental and not yet supported by a +published YAQS paper. The updated figure was inspected. Other figures were +inspected in the earlier comparison build. The Markdown child retains six SVG +MIME-priority warnings for chunk 4. This used warm numerical caches; final +full-site and cold RTD validation remain pending. + +Current plot, pulse-sweep data, package versions, timings, and logs: +[/tmp/yaqs-quickstart-pulse-sweep/report.md](/tmp/yaqs-quickstart-pulse-sweep/report.md). + +Earlier independent checks and longer-horizon training trials: +[/tmp/yaqs-quickstart-generalization/report.md](/tmp/yaqs-quickstart-generalization/report.md). + +### Analog-digital quickstart addition: 2026-10-09 + +The quickstart now includes the 20-site XY excitation echo with three occupation +heatmaps: free evolution, refocusing, and refocusing with Pauli-Z dephasing at +rate 0.2. All panels share one scale. The setup uses public imports, separate +simulator initialization, default parallel execution, and 32 noisy trajectories. +Plotting code is folded, and the section links to the detailed program guide. + +All 17 production cells matched the executed notebook. The complete quickstart +executed in 160.9 seconds; the new simulation cell took 15.8 seconds. Numerical +checks passed for all seven workflows. The noiseless return was 0.99994, and the +dephased return was 0.341 against an independent Lindblad reference of 0.350, +with estimated standard error 0.059. Checks covered every new heatmap sample, +the plotted arrays, input preservation, segment continuity, observable order, +trajectory means, and excitation conservation on each trajectory. + +The isolated strict HTML build passed. The new figure was inspected, all seven +SVG figures rendered, and Markdown reused execution. LLM files remain available. +The Markdown child retains seven SVG MIME-priority warnings for chunk 4. This +run used the existing Python 3.12 documentation environment with one numerical +thread, two workers, and warm compilation caches. Linked page contents were +stubbed; full-site and cold RTD validation remain pending. + +Preview, numerical checks, raw echo trajectories, independent references, CSV +data, figure exports, source hash, versions, timings, and logs: +[/tmp/yaqs-quickstart-echo/report.md](/tmp/yaqs-quickstart-echo/report.md). + +## Chunk 3: Fill practical gaps and correct claims + +- [ ] Add one supported-combinations overview. Consolidate representation, + noise, circuit, diagnostic, ensemble, piecewise-program, and + characterization restrictions. Link to existing guides for details. +- [ ] Add a short results guide covering observable ordering, array axes, time + grids, counts, requested versus executed trajectory counts, diagnostics, + spectra, final states, and program segments. Explain that averaged noisy + Schmidt spectra describe pure trajectories, not a mixed-state spectrum. +- [ ] Explain when outputs are populated and which combinations are supported. + Keep result-field details accurate without adding a persistence API. +- [ ] Document pickle as trusted, temporary, same-version checkpoint storage. + Avoid promises of portable or versioned persistence. +- [ ] Add a complete parallel script example with an + `if __name__ == "__main__":` guard. Explain notebook execution separately + and keep automatic process-context guidance current. +- [ ] Correct reproducibility claims, including the limits of `State(seed=...)`. + Use supported seeds where examples need repeatable stochastic results. +- [ ] Correct claims about configuration mutation and automatic backend + selection against the implementation. +- [ ] Supply existing evidence for the equivalence-performance crossover claim, + including the referenced benchmark script, or remove the claim. Describe + the configured automatic cutoff as a heuristic. +- [ ] Use supported public imports in ordinary examples. Mark intentionally + documented low-level interfaces clearly without exporting extra helpers + solely for documentation. + +Acceptance: users can choose a supported workflow, interpret its output, and +understand relevant limitations. No major feature needs another broad tutorial. + +## Chunk 4: Simplify navigation and complete validation + +- [ ] Align the documentation homepage's title and introduction with the README. + Remove the unsupported "under a minute" quickstart promise. Shorten the + 21-row learning-path table to the main user tasks. Describe executable and + static examples accurately. +- [x] Regroup sidebar navigation while preserving existing page URLs: + +| Group | Contents | +| --------------------------------- | --------------------------------------------------------------------------- | +| Start here | Installation and quickstart | +| Simulation setup | States, Hamiltonians, noise, representations, presets, execution, results | +| Simulation workflows | Analog, circuits, shots, analog-digital programs | +| Characterization and verification | Memory, noise fitting, circuit equivalence | +| Advanced examples | Ensembles, scheduled jumps, custom gates, hardware, experimental surrogates | +| Reference and contributing | API, citations, changelog, upgrading, development, support | + +- [ ] Curate API navigation around the stable public interface. Remove duplicate + object indexing and unresolved targets. Keep implementation helpers from + overwhelming the public reference and preserve needed canonical links. +- [ ] Fix the unsupported Mermaid directive, notebook metadata and lexer + warnings, document and method references, and remaining citation or + included-file references. The bibliography directive was repaired in the + README/reference update; verify the merged version rather than repeat it. +- [ ] Add a fast strict documentation check to CI. Pass a fresh + `sphinx-build -E -a -n -T -W --keep-going` check without blanket warning + suppression. Scope necessary external-reference exceptions narrowly. +- [ ] Execute all documentation notebooks in a clean environment, including + optional examples. Validate supported imports and meaningful outputs + without duplicating the numerical test suite. Record execution timings. +- [ ] Run `uvx nox -s docs -- -b linkcheck`. Fix broken project links and record + necessary exceptions for unavailable external sites. +- [ ] Inspect rendered desktop and narrow-screen pages: navigation, code, + figures, tables, diagrams, API links, citations, and release notes. +- [ ] Run `uvx nox -s lint` after each batch of changes. +- [ ] Confirm that RTD builds the final reviewed documentation successfully. +- [ ] Complete Aaron's documentation review and resolve substantive findings. + +Acceptance: users can find each supported capability through the sidebar and +task links. Strict checks, example execution, link checking, rendered-page +inspection, and a cold RTD build pass for the reviewed commit. + +### Navigation validation: 2026-10-09 + +The sidebar uses the six groups above with short labels. All 29 existing page +targets remain present once, and page URLs are unchanged. The results guide can +join Simulation setup when chunk 3 adds it. + +A full HTML render with notebook execution disabled succeeded. Rendered sidebar +checks passed on the homepage, quickstart, equivalence guide, and API root: each +page retains all six groups in order and links to all existing targets. This +check omitted external inventories and did not use `-W`; the build retained 477 +documentation warnings. Full strict validation and responsive browser inspection +remain pending. Full lint and the planning-file hooks passed. + +Navigation preview and evidence: +[/tmp/yaqs-docs-navigation/html/index.html](/tmp/yaqs-docs-navigation/html/index.html), +[/tmp/yaqs-docs-navigation/record.json](/tmp/yaqs-docs-navigation/record.json), +and +[/tmp/yaqs-docs-navigation/build.log](/tmp/yaqs-docs-navigation/build.log). diff --git a/PAPER_WRITING.md b/PAPER_WRITING.md new file mode 100644 index 000000000..45d734153 --- /dev/null +++ b/PAPER_WRITING.md @@ -0,0 +1,354 @@ +# Scientific Paper Writing Guide + +Use this guide to draft or revise technical research papers for clarity, coherence, and scientific precision. The goal is not to make every paper sound the same. It is to make the reasoning easy to follow while preserving the authors' voice and the subject's necessary technical detail. + +Treat revision as a careful human line edit, not as a general rewrite. Simplify the explanation itself rather than mechanically replacing technical words with supposedly simpler synonyms. + +## 1. Start with the scientific story + +Before editing sentences, write the paper's story in five to seven plain statements: + +1. What broad problem matters? +2. What is already known? +3. What remains unresolved? +4. What does this paper do? +5. What evidence answers the unresolved question? +6. What is the main result? +7. What remains limited or unknown? + +Every major section should advance this story. Remove, shorten, or relocate material that does not help the reader understand or evaluate it. + +The storyline is not a list of everything done during the project. It is the shortest defensible chain from the motivating problem to the conclusion supported by the evidence. + +### Use a V-shaped structure + +The paper should narrow and then widen: + +1. Begin with the general scientific problem and why it matters. +2. Narrow to the specific gap, construction, and tests addressed by the paper. +3. End by returning to the broader meaning, limitations, and practical consequences. + +Use the same shape within major sections whenever possible. Open with the high-level question and its role in the paper, move into the necessary technical detail, and close with the answer and a transition to the next question. This is a logical structure, not a demand for artificial symmetry. + +### Respect the reader's knowledge order + +Present ideas in the order needed to understand them. Do not refer to a proof, proposition, mechanism, result, or technical distinction before it has been introduced. The abstract and introduction may preview the main outcome in ordinary language, but they should not depend on later notation or unexplained labels. + +A transition should normally identify the question that remains after the current section. It should motivate the next section without giving its detailed answer in advance. At every transition, ask what the reader knows at that point and what they need to learn next. + +## 2. Separate prior work from the present contribution + +Make the boundary unmistakable. + +- Describe established knowledge in neutral, factual prose. +- Explain the specific gap before presenting the new work. +- Begin the contribution with a clear transition such as “Here, we…” or “In this work, we…”. +- Use active voice for the authors' choices, derivations, tests, and conclusions. +- Make the change visible in both structure and voice. Prior work may be described mostly in neutral or passive language where natural. The present contribution should switch clearly to active language using “we.” +- Do not hide this change in the middle of a paragraph. Start a new paragraph, subsection, or section when the scale of the paper permits it. +- Do not imply novelty merely by describing standard material in new terminology. + +A useful introduction progression is: + +1. Motivate the general problem. +2. Explain the established approaches relevant to that problem. +3. Identify the limitation or unresolved question. +4. State what this paper contributes. +5. Preview the principal result and its scope. + +Do not turn the contribution paragraph into a checklist of sentences beginning with “We present,” “We derive,” or “We demonstrate.” Group related contributions into a short argument. + +### Cite ideas rather than narrating authors + +Discuss the scientific concept or result and place the citation directly after it. Avoid humanities-style narration built around researchers' names. + +Prefer: + +> Rank-adaptive tensor-network integrators enlarge the represented space before truncation [7, 8]. + +Avoid: + +> Smith and Jones introduced a rank-adaptive tensor-network integrator [7]. + +Use author names only when the identity itself is relevant, such as a named theorem, a direct historical dispute, or wording that cannot otherwise be attributed clearly. A related-work section should compare assumptions, constructions, and results rather than recounting who did what. + +## 3. Match every claim to evidence + +For each central claim, ask: + +- What result supports it? +- Does that result establish the claim directly, or merely illustrate it? +- What alternative explanation has been ruled out? +- What qualification must remain? + +Use the weakest claim that communicates the actual result completely. + +Examples of important distinctions: + +- Numerical tests can validate an implementation or illustrate an identity. They do not prove a universal theorem. +- Decreasing error over a short timestep sequence shows empirical refinement. It does not establish a formal convergence order. +- One controlled example can identify a concrete failure mode. It does not show that the failure occurs for every model or implementation. +- Equal tolerances do not necessarily imply equal cost, equal discarded weight, or an equal-resource comparison. +- A method working after a correction does not imply that it outperforms established alternatives. +- Failure to observe an advantage is a result, not a reason to manufacture a stronger performance story. + +Preserve limitations wherever they affect interpretation. Simpler prose must not become stronger prose. + +## 4. Write for an informed non-specialist + +Assume the reader understands the general field but not this paper's particular construction. + +- Explain the problem before naming detailed mathematical objects. +- Introduce notation only when it becomes useful. +- Define each specialized term before relying on it. +- Prefer language already used in the closest literature. +- Keep established terminology when it is both precise and readable. Do not rename a known object merely to make the paper appear novel. +- Use an ordinary description instead of inventing a term for every implementation choice. +- Reserve dense terminology for sections where mathematical precision requires it. +- When two formulations are equally precise, choose the one that is more accessible. + +For example, first write “project the coefficients into the updated basis.” Introduce a shorter formal name only if the operation appears often enough that the name genuinely helps. + +A term should earn its place. Name a concept when the name improves later reasoning, not simply because the concept exists. + +## 5. Use simple, exact language + +### Sentence construction + +- Put one main idea in each sentence. +- Prefer concrete verbs such as “keep,” “remove,” “compare,” “project,” “increase,” and “compress.” +- Break up sentences containing several conditions, contrasts, or qualifications. +- Avoid noun stacks with several technical modifiers. +- Keep the subject and verb close together. +- State the result before discussing secondary details. +- Use “we” naturally, but not at the beginning of every sentence. + +### Words and structures to use sparingly + +- Flowery adjectives and promotional modifiers +- “Crucial,” “groundbreaking,” “robust,” “comprehensive,” and “systematic” unless they have a precise meaning +- “Highlights,” “underscores,” “reveals,” “offers insight into,” and “plays a pivotal role” when the result can be stated directly +- Repeated “not merely X, but Y” constructions +- Em dashes +- Heavy use of colons and semicolons +- Formal synonyms where an ordinary word is clearer +- Repeated three-part lists used only for rhetorical rhythm + +Replace claims about importance with the reason the result matters. Replace claims that a result “demonstrates” something with the observation and its supported interpretation. + +Avoid both extremes: prose should not be ornate, but it should not read like a sequence of clipped notes. Transitions should show how one question leads to the next. + +## 6. Build sections around reader questions + +At the beginning of each major section, state in one or two sentences why it is needed. Then move from that overview into the technical detail. At the end, return to the section's main answer and connect it to the next question without introducing unexplained material. + +Transitions must follow the reader's current knowledge. Do not write “as proved below,” interpret a result that has not yet been shown, or use terminology belonging to the next section. If later material must be previewed, describe only the motivating question in language already available to the reader. + +For each theoretical subsection, make clear: + +1. What problem is being formalized? +2. What assumptions are required? +3. What is proved? +4. Why is the result needed later? +5. What does the result not establish? + +For each numerical study, use this order: + +1. The question being tested +2. The construction of the test +3. The observed result +4. The conclusion supported by the observation +5. The conclusion that cannot be drawn + +Do not begin with a page of parameters before stating why the calculation exists. Give enough setup for reproducibility, then keep the main observation visible. + +## 7. Design the abstract as a compact argument + +The abstract should be understandable without the Methods section. A reliable structure is: + +1. The broad problem +2. The unresolved issue +3. The paper's approach +4. The central theoretical or methodological contribution +5. The decisive evidence +6. The main conclusion, including an important negative result if relevant + +Avoid: + +- notation; +- proposition or equation numbers; +- new terminology that is unnecessary to understand the result; +- a list of every experiment; +- claims of novelty or importance unsupported by the abstract itself; +- detailed implementation labels when a plain description works. + +The abstract should explain what happens and why it matters, not reproduce the manuscript's internal vocabulary. + +## 8. Keep the introduction progressive + +The introduction should follow the paper's V shape. It begins with the broad problem, narrows to the exact unresolved question, and ends by placing the contribution and main finding back in the wider context. + +It should not spoil results before the reader understands the problem, but it should still state the paper's main finding plainly. Preview the conclusion at a level appropriate for the introduction. Do not refer to a later proof, proposition number, figure, or technical mechanism before establishing the concepts needed to understand it. + +Each paragraph should perform one role: + +- establish the setting; +- narrow to the relevant methods; +- explain the unresolved problem; +- identify the contribution; +- summarize the evidence and conclusion. + +Do not introduce notation, algorithm labels, or fine distinctions before the reader needs them. Cite closely related work where the comparison becomes relevant, and state exactly how the present contribution differs. Frame these citations around the methods or findings rather than the researchers' names. + +## 9. Present methods in dependency order + +Definitions and operations should appear before anything that depends on them. + +- Begin with the minimum common notation. +- Explain each representation before manipulating it. +- Introduce the algorithm in the same order in which it operates. +- Separate exact mathematical statements from implementation conventions. +- Distinguish what holds before approximation or compression from what may fail afterward. +- State whether a choice is mathematically required, one valid implementation, or merely the convention used in the paper. + +If a passage cannot be simplified without losing precision, keep the formal language and add one plain explanatory sentence around it. + +## 10. Make results answer the paper's claims + +The Results section should be an evidence chain, not a log of completed computations. + +A useful progression for a methods paper is: + +1. Structural or unit-level checks of the derived properties +2. Independent correctness validation of the complete implementation +3. A controlled test isolating the proposed mechanism +4. Ablations ruling out plausible alternatives +5. A restrained comparison with established methods + +Report the computational environment and software used. Provide enough information to reproduce the study, including the relevant code revision, parameters, reference construction, and accuracy metrics. + +Use negative results directly. If a method is less accurate, slower, or less stable in the tested regime, state that result and adjust the paper's claim. Do not compensate with vague claims of potential superiority. + +## 11. Make figures and tables carry arguments + +Every figure or table should answer a question that matters to the storyline. + +Before adding one, ask: + +- What question does this item answer? +- Is a plot, table, or sentence the clearest format? +- What should the reader conclude from it? +- Is that conclusion stated in the surrounding text? + +Caption structure: + +1. A short bold title +2. What question is addressed or what is shown +3. The essential setup needed to interpret it +4. The main answer +5. Any qualification necessary to prevent overinterpretation + +Captions should be understandable on their own but should not reproduce an entire Results paragraph. Introduce every figure and table before interpreting it. Use the same terminology in the caption, legend, table entries, and main text. + +Use plots for trends, tradeoffs, or comparisons across several values. Use tables for exact values, configurations, or stage-by-stage states. Do not convert a table into a plot merely to make the paper look more visual. + +## 12. Use the discussion for interpretation + +Do not repeat the paper section by section. Answer: + +1. What was learned? +2. Why does it matter? +3. Which conclusions are limited to this setup? +4. What remains unresolved? + +Separate limitations caused by the implementation from limitations of the underlying method. A plausible but unsuccessful algorithmic choice is not automatically a coding error. + +End with a restrained statement of practical or scientific relevance. Do not advertise a general advance when the evidence establishes a focused one. + +## 13. Preserve the authors' voice + +Use prior papers by the same authors to calibrate paragraph length, level of explanation, and preferred transitions. Do not copy their wording or force the current paper into an unrelated template. + +Avoid repeated global rewrites. They tend to flatten the voice, introduce new terminology, and produce polished but generic prose. Once the structure is sound, prefer local edits made while reading the paper in order. + +Good scientific prose should sound like a knowledgeable author explaining a result carefully, not like a press release and not like an instruction manual. + +## 14. Editing workflow + +### Pass 1: Story and claims + +- Write the paper's story in plain language. +- List the central claims and the evidence supporting each one. +- Remove unsupported claims and disconnected results. +- Check novelty against the closest literature. + +### Pass 2: Structure + +- Reorder sections and paragraphs by conceptual dependency. +- Check the paper's V shape from broad motivation to specific evidence and back to broader interpretation. +- Apply the same overview-detail-transition pattern within major sections where it helps. +- Make the prior-work/new-work boundary explicit. +- Ensure that the shift to the present work is visible in both section structure and active voice. +- Ensure every section answers a necessary question. +- Remove forward references that require knowledge the reader does not yet have. +- Rewrite transitions so that each section motivates the next question without prematurely answering it. +- Move secondary derivations and diagnostics to appendices when appropriate. + +### Pass 3: Language + +- Remove filler, hype, and unnecessary adjectives. +- Simplify terminology and long sentences. +- Replace vague verbs with concrete statements. +- Improve transitions without adding rhetorical padding. + +### Pass 4: Evidence presentation + +- Check every figure, table, and caption against its reader question. +- Confirm that numerical comparisons use appropriate metrics and controls. +- Make negative results and limitations explicit. + +### Pass 5: Human read-through + +Read the manuscript from beginning to end without editing individual sentences on the first pass. Mark every point where the reader must stop, infer a missing connection, or remember an undefined term. Then repair those points locally. + +## 15. Non-negotiable editing constraints + +Unless an actual inconsistency is found, do not change: + +- equations; +- numerical values; +- citations; +- propositions or mathematical conditions; +- algorithm definitions; +- figure data; +- conclusions required by the evidence. + +Flag suspected inconsistencies instead of silently correcting them. Record any scientific changes separately from language edits. + +## 16. Final audit + +Read the paper in order as if unfamiliar with the work. Confirm that: + +- the complete story can be summarized in a short paragraph; +- every section advances that story; +- the paper narrows from general motivation to its specific contribution and widens again to interpretation; +- major sections open with an overview, provide the required detail, and close with an answer or bridge; +- every specialized term is explained before use; +- no paragraph depends on a later definition or result; +- no transition spoils a proof, result, or mechanism that the reader has not encountered; +- the boundary between known work and new work is obvious; +- the present contribution is marked by a clear structural and active-voice shift; +- literature is discussed through concepts and findings rather than author-name narration; +- each major claim has visible supporting evidence; +- limitations are stated where they affect interpretation; +- the abstract is understandable without the Methods; +- terminology is consistent across prose, equations, figures, and tables; +- captions state what is shown and what the reader should conclude; +- negative results have not been hidden or rhetorically softened; +- equations, references, numerical values, and cross-references remain intact; +- code and data availability statements describe what is actually accessible; +- the source compiles without new warnings or layout problems. + +## Recommended instruction for a writing assistant + +> Edit this manuscript for a coherent scientific storyline, precise claims, and natural technical prose. Write for a reader who understands the broader field but not this paper's particular construction. Use the simplest language that preserves the mathematics. First identify the paper's problem, gap, contribution, evidence, main conclusion, and limitations. Give the paper a V-shaped structure that moves from the broad problem to the specific contribution and evidence, then returns to the broader meaning and limitations. Use the same overview-detail-transition pattern within major sections where appropriate. Ensure that every section advances the storyline in the order the reader needs. Do not mention proofs, results, distinctions, or terminology before they have been introduced. Transitions should motivate the next question without spoiling its answer. Clearly separate established work from the present contribution through a noticeable structural change and a shift from mostly neutral or passive prose to active “we” language. Discuss previous literature through concepts and findings followed by citations, not through narration centered on author names. Define specialized terms before use, prefer established language from the closest literature, and avoid inventing labels that do not help later reasoning. Remove filler, promotional adjectives, em dashes, excessive colons or semicolons, repetitive rhetorical structures, and long jargon-heavy sentences. Preserve equations, numerical values, citations, propositions, and mathematical conditions unless an inconsistency is found; flag such inconsistencies rather than silently changing them. Match every claim to its evidence and retain all necessary qualifications. Organize each numerical study around its question, setup, observation, supported conclusion, and limitation. Make every figure and table answer a clear question, with a concise caption that states the main result. Finish with a complete read-through for logical dependencies, terminology, claim scope, compilation, and layout. Return the revised source with a short change log of substantive structural or scientific edits, not a catalogue of wording substitutions. diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..8861bb881 --- /dev/null +++ b/TODO.md @@ -0,0 +1,439 @@ +# YAQS 1.0 Release TODO + +## Goal and scope + +Release the existing YAQS feature set with correct numerical behavior, a clear +stable API, and working documentation. Add no new features before 1.0. HDF5 +persistence is outside this release. + +Complete correctness repairs and validation first. Then Aaron reviews the code +and reads and updates the README and documentation. Resolve findings from those +reviews before validating and publishing the release candidate. + +Chunks 1 through 5 contain the required software-release work. Optional cleanup +and SciPost paper work have separate sections below. + +## Current status + +The review on 2026-10-07 covered local `remove-legacy` at `9b066659` and GitHub +`main` at `e22c3ef4`. The local branch was merged through PR #609; later changes +on `main` include dependency and tooling updates. + +- [x] Establish one public spatial ordering: site 0 is the least-significant + subsystem. Cover asymmetric states, operators, local noise, observables, + mixed physical dimensions, and memory-characterization conversions. +- [x] Use uncapped direct process-tensor construction by default. Warn that + finite branch caps are experimental and document exponential cost. +- [x] Validate process-tensor shape, conditional-state positivity, Bloch bounds, + and information metrics. Cover dense and analytic references. +- [x] Harden public simulation controls, mutable time grids, Hamiltonian inputs, + real-valued results, state inputs, tensor shapes, and cross-object + physical dimensions. Test public errors under normal Python and + `python -O`. +- [x] Reject unsupported dense-representation diagnostics and multi-time output + combinations. Preserve sampled and final Schmidt spectra for MPS runs, + including noisy trajectories and direct MPS spectrum evaluation. +- [x] Remove legacy solver fallbacks and preserve explicit backend selection. +- [x] Separate local observables from gate classes and support explicit + final-state full-chain expectations through `MPS.expect_mpo()`. +- [x] Pin the current top-level exports with `tests/test_public_api.py`. +- [x] Make core-only imports silent and include `py.typed` in the wheel. +- [x] Add serial/parallel reproducibility, explicit process-pool, spawn-process, + and JIT-enabled tests. Keep slow rate recovery in the manual release tier. +- [x] Pass the local Python 3.14 serial suite: 3,288 passed, 3 skipped, 3 + deselected, and 5 xfailed. Pass the manual rate-recovery test separately. +- [x] Pass current-main CI on Ubuntu, Ubuntu ARM, macOS, and Windows, including + Linux and Windows JIT checks and the clean-wheel check. +- [x] Build and inspect the current-main sdist and wheel. + +These checks apply to the named commits. They do not replace validation of the +final candidate. Strict documentation has known content defects. Fresh clean +notebook execution and external link checking remain release requirements. + +## Working rules + +- Keep each repair small enough for a focused review. +- Add or update behavioral regressions for every code change in the test tree + owned by the affected component. Test the supported public contract. +- Run targeted tests during development and `uvx nox -s lint` after each batch + of changes. Resolve substantive failures before moving to the next chunk. +- Update `CHANGELOG.md` and `UPGRADING.md` for user-facing or breaking changes. + Include required PR references and author links, and disclose AI assistance in + any authorized pull request. +- Preserve independent numerical references and slow scientific regressions. + Remove only genuine duplication or unnecessary cost. +- Preserve Python 3.11 through 3.14 testing on every supported CI platform. +- Fix public-boundary validation and numerical defects without adding repeated + whole-network checks to numerical loops or rewriting the package structure. +- Keep unsupported, approximate, and experimental combinations explicit. +- Do not modify template-managed files directly. Address template changes + upstream or state package-specific behavior in repository-owned documentation. + +## Chunk 1: Correctness repairs and validation + +Complete this chunk before Aaron's code and documentation reviews. + +### 1.1 Make Schmidt-spectrum output agree with the supported contract + +Spectrum observables retain trajectory, sample, and coefficient axes. MPS analog +and digital runs, deterministic list ensembles, and simulation programs support +sampled and final-only spectra, including noisy pure trajectories. + +- [x] Define result shapes: `(num_traj, num_samples, 500)` for trajectories and + `(num_samples, 500)` for means. Preserve descending coefficients and `NaN` + padding from direct `MPS.get_schmidt_spectrum()`. +- [x] Repair worker buffers and result storage without changing scalar result + shapes or observable ordering. +- [x] Average trajectory coefficients with missing ranks counted as zero. + Preserve padding when every trajectory lacks a coefficient. Explain that + this mean is not a Schmidt spectrum of the mixed state. +- [x] Stitch program mean spectra along the sample axis and retain individual + trajectories in segment results. +- [x] Cover both analog orders, digital checkpoints, changing ranks, mixed + observable ordering, sampled and final-only output, deterministic + ensembles, and noisy serial/parallel execution. Preserve direct MPS tests + and independent dense SVD and analytic trajectory references. +- [x] Update observable and result documentation, examples, and release notes. + Remove the obsolete spectrum-concatenation branch and its tests. + +Acceptance: scheduled spectra agree with independent references, preserve their +axes across execution paths, and work for noisy trajectories without requesting +a representative final state. + +### 1.2 Honor noise-optimizer controls and report losses accurately + +Noise fitting applies the validated optimizer limits and reports the loss of the +supplied initial model separately from candidate history. + +- [x] Pass `max_iter` to bounded scalar search. Document CMA-ES generations, + SciPy's evaluation stopping limit, and the two-evaluation startup case. +- [x] Evaluate the initial model before optimization and store `initial_loss`. + Make `sqrt_loss_before()` use this baseline. Keep candidate history + separate from baseline and final-fit evaluations. +- [x] Add public characterization regressions with one and two fitted parameters + and small iteration limits. Cover scalar and CMA-ES dispatch. +- [x] Compare the baseline with independent analytic Pauli-noise trajectories. + Preserve fitted-rate and dynamics recovery coverage. +- [x] Update result and optimizer docstrings, the digital-twin example, and + release notes. + +Acceptance: optimizer controls affect execution, and the before-optimization +loss describes the supplied initial model. Fitted-rate recovery still passes. + +### 1.3 Honor quiet simulation during shot readout + +Simulator progress controls cover trajectory and shot-readout bars, including +program segments. Progress remains visible by default. Simulator shot readout +runs within each trajectory and does not create another process pool. + +- [x] Make shot readout respect `show_progress=False` through `Simulator.run`. +- [x] Document execution controls: `parallel` and `max_workers` govern + trajectory pools, and simulator shots run serially within each trajectory. + Direct `MPS.measure_shots()` retains parallel sampling. +- [x] Remove nested shot pools from combined noisy observable-and-shot runs. + `parallel=False` keeps simulator readout in the current process. +- [x] Add regressions for quiet and visible readout, direct MPS sampling, + program segments, worker limits, and shot counts. Preserve seeded + trajectory tests. + +Acceptance: documentation runs can suppress all progress bars, default progress +remains visible, and shot readout cannot bypass simulator worker controls. + +### 1.4 Verify existing workflows after the repairs + +Use existing tests and references first. Add tests for uncovered supported +contracts or concrete regressions, not for a new feature matrix. + +- [x] Run targeted regressions for the repairs above and public validation. +- [x] Check asymmetric analog evolution across MPS/TJM, vector/MCWF, and + density-matrix/Lindblad paths against independent dense references. +- [x] Check noisy analog dynamics against analytic or Lindblad references. + Preserve jump-probability and trajectory-convergence checks. +- [x] Check default-MPO digital evolution against Qiskit, including long-range + and multi-qubit gates, observable ordering, and shot counts. +- [x] Check list ensembles, piecewise evolution, and mixed programs against + existing independent or exact small-system references. +- [x] Check memory characterization, default direct process tensors, conditional + responses, information metrics, and noise fitting against existing + references. +- [x] Re-run the full serial suite with numerical-library thread limits. +- [x] Pass the supported OS/Python CI matrix and minimum-dependency tests. + Review warnings from minimum-dependency runs, not only their exit status. +- [x] Pass Linux and Windows JIT tests with compilation enabled and no coverage. +- [x] Pass `uvx nox -s release-tests` and `uvx nox -s lint`. +- [x] Record commit, environment, commands, outcomes, and accepted limitations. + Account for every skip and xfail. The five circuit-TDVP rank-growth xfails + may remain only with accurate documentation and a supported default route. +- [x] Replace automatic Linux `fork` with `forkserver`. Preserve explicit start + methods and real process-pool coverage. + +Acceptance: no advertised stable workflow has an unresolved failure. Relevant +independent references support numerical agreement. + +Validation on 2026-10-08 covers `pool-fix` at `7d1e5c3a` on Linux with Python +3.14.2. The full serial suite passed with 3,378 passed, 3 skipped, 3 deselected, +and 5 xfailed in 135.58 seconds. The skips are invalid site/chain combinations. +The five xfails cover documented rank-growth limits in the optional circuit-TDVP +paths; the default MPO route passes. The three deselected tests passed +separately: one manual rate-recovery test and two JIT tests. Lint passed. + +The serial run used numerical thread limits of one, `YAQS_MAX_WORKERS=2`, +`NUMBA_DISABLE_JIT=0`, and pytest +`-n 0 -p no:cacheprovider -m 'not release and not jit' --durations=30`. +Dedicated integration tests retain explicit process pools. Nox ran +`release-tests` and `jit-tests` without coverage. Environment versions, +commands, XML results, logs, and accepted limitations are recorded in +`/tmp/yaqs-1-4-validation-7d1e5c3a/`. + +[CI run 37695876971](https://github.com/munich-quantum-toolkit/yaqs/actions/runs/37695876971) +passed for the same commit. Python 3.11 through 3.14 passed on Ubuntu x86, +Ubuntu ARM, macOS, and Windows. Both Ubuntu minimum-dependency runs passed, as +did Linux and Windows JIT tests and the Linux wheel check. + +The earlier Python 3.14 minimum-dependency CI run emitted 14 warnings about +`fork` from multi-threaded processes. Automatic Linux context selection now uses +`forkserver`. Explicit start methods remain supported. + +Local follow-up validation on 2026-10-08 covers `7d1e5c3a` plus the working-tree +repair. The full Python 3.14 suite passed with 3,382 passed, 3 skipped, 3 +deselected, and 5 xfailed in 141.86 seconds. The minimum-dependency suite passed +with 3,382 passed, 3 skipped, and 5 xfailed in 46.00 seconds. Both runs treated +`multiprocessing.popen_fork` deprecation warnings as errors; neither reported +fork warnings. Minimum dependencies include Numba 0.63.0, NumPy 2.3.2, SciPy +1.16.1, and Torch 2.9.0 CPU. All optional tests remain included. + +The 36 targeted checks passed on Python 3.11 and 3.14. They include a live +parent thread with two real workers, serial/parallel shot counts, seeded +noise-fitting trajectories, and automatic and explicit-spawn equivalence +checking. Lint passed. Commands, source checksums, environments, timings, and +logs are in `/tmp/yaqs-forkserver-validation-7d1e5c3a/`. The supported OS/Python +CI matrix must validate the updated branch before merge. + +### 1.5 Continue trajectory RNG streams across MCWF memory segments + +MCWF memory characterization uses one continuous RNG stream per trajectory +across evolution segments. Independent noisy-channel references protect +conditional process-tensor responses and joint weights. + +- [x] Let consecutive MCWF segments reuse the worker-owned trajectory RNG. +- [x] Forward that RNG through the memory-characterization backend. +- [x] Compare noisy dense process-tensor conditional responses and joint weights + with an analytic channel across two nonzero evolution slots. +- [x] Preserve single-segment reproducibility and the existing TJM RNG contract. + +Acceptance: seeded MCWF process-tensor predictions agree with independent noisy +channel references within justified sampling error. + +## Chunk 2: Aaron's code review and API freeze + +Start after chunk 1 passes. Review substantive correctness and maintainability. +Avoid broad refactoring for appearance or module size. + +- [ ] Review simulator dispatch, state handoff, parameter mutation, result + allocation, time grids, observable ordering, and output aggregation. +- [ ] Review MPS/MPO conversions, site ordering, physical dimensions, + orthogonality-center tracking, normalization, and truncation contracts. +- [ ] Review analog integration, dissipation, jump selection, and supported + MCWF, Lindblad, ensemble, and piecewise paths. +- [ ] Review digital gates, long-range operations, measurement, noise + application, and equivalence-checking results and approximation claims. +- [ ] Review memory and noise characterization, process-tensor physicality, + conditional responses, information metrics, and optimizer contracts. +- [ ] Review public validation, exception behavior, RNG streams, optional + imports, parallel execution, and optimized-Python behavior. +- [ ] Review automated coverage against these contracts. Preserve independent + references and distinguish unit tests from scientific reproduction. +- [ ] Record and resolve major findings with focused tests. Re-run affected + checks before accepting each repair. +- [ ] Define stable import paths for existing facades, result types, and + process-tensor types. Use existing paths where practical; do not expand + the public surface merely to flatten the package. +- [ ] Pin the final supported boundary in public API tests. Keep workers, + encoders, backend helpers, and other implementation details outside it. +- [ ] Confirm that supported combinations work or have clear errors, and every + approximation or experimental path has a stated limitation. +- [ ] Complete human review of the code and materially AI-assisted changes. + +Acceptance: Aaron understands the major numerical and public contracts and has +no unresolved major finding. The stable API can carry compatibility promises +throughout 1.x. + +## Chunk 3: Aaron's README and documentation review + +### 3.1 Read and update the user-facing text + +- [ ] Read and update the README: installation, first-use examples, feature + scope, limitations, support, and software and method citations. +- [ ] Read the installation and all user guides. Check each guide against the + final implementation and supported imports. +- [ ] Review the API reference for accurate signatures, result fields, return + types, defaults, units, ordering, shapes, and error behavior. +- [ ] Add one supported-combinations table for analog representations, digital + MPS simulation, list ensembles, piecewise and mixed programs, local and + bitstring observables, shot readout, and characterization backends. +- [ ] State process-tensor exponential cost, operations that densify, finite-cap + experimental status, and circuit-TDVP rank-growth limits. Keep the default + circuit MPO route and explicit `MPS.expect_mpo()` contractions clear. +- [ ] Correct `UPGRADING.md`: `Observable` exposes `.type`, not `.kind`. +- [ ] Document existing `Result` fields, requested versus executed trajectory + counts, time and observable axes, shot totals, diagnostics, final states, + multi-time outputs, and nested program results. Add no result API + features. +- [ ] Describe pickle as trusted, same-version, temporary checkpoint storage. Do + not promise portable or versioned result persistence. +- [ ] State the tested Python range as 3.11 through 3.14 and confirm support + ownership and the maintenance policy. +- [ ] Use consistent terms and precise prose. Separate method-paper citations + from the software citation and bound numerical claims by their evidence. +- [ ] Publish existing evidence for the claimed equivalence-checking eight-qubit + crossover, including the referenced benchmark script, or remove the claim. + Describe the configured automatic cutoff as a heuristic where appropriate. +- [ ] Read and finalize release notes for all user-facing and breaking changes, + with PR references and all contributing authors. + +### 3.2 Verify examples and the documentation build + +- [ ] Use supported imports in first-use and ordinary user examples. Mark any + intentionally documented low-level API clearly. +- [ ] Give stochastic examples fixed seeds where reproducibility matters. Use + documented valid seeds, including zero where supported. +- [ ] Add tolerances and independent reference assertions to the small + representation-comparison example. Verify other release examples' expected + outputs without duplicating the numerical test suite. +- [ ] Fix the unknown Mermaid directive in `docs/index.md` and malformed + bibliography directive in `docs/references.md`. +- [ ] Fix document, method, citation, and included-file references, including + first-use links and the `State.from_mps` reference. +- [ ] Curate AutoAPI to remove duplicate objects and unresolved targets and + distinguish stable API from implementation details. +- [ ] Add a fast strict documentation check to CI. Pass a fresh + `sphinx-build -E -a -n -T -W --keep-going` build without blanket warning + suppression. Keep necessary external-reference exceptions narrow. +- [ ] Execute every documentation notebook and README workflow in a clean + environment using a wheel built from the reviewed checkout. Cover optional + examples with declared extras and keep the source tree off the import + path. Repeat these checks on the tagged release candidate in chunk 5. +- [ ] Run `uvx nox -s docs -- -b linkcheck`. Resolve broken project links and + record necessary exceptions for unavailable external sites. +- [ ] Inspect rendered desktop and narrow-screen pages: code blocks, diagrams, + figures, tables, navigation, API links, and included release notes. +- [ ] Confirm that Read the Docs builds the reviewed documentation successfully. +- [ ] Complete Aaron's own README and documentation review and resolve findings. + +## Chunk 4: Compatibility policy and release metadata + +- [ ] Commit to compatibility for the documented stable API throughout 1.x. + Remove the changelog exception permitting breaking minor releases, or + limit that exception explicitly to pre-1.0 versions. +- [ ] Finalize the 1.0 changelog and migration instructions from the last + release. +- [ ] Change the final package development-status classifier from Beta to the + appropriate stable-release status. +- [ ] Add `CITATION.cff` for the software release and align citation + instructions. +- [ ] Verify authors, maintainers, license, supported Python versions, extras, + package metadata, and repository, changelog, and release URLs. +- [ ] Plan the software archive and DOI or release identifier. Add the final + identifier to citation and release metadata when it becomes available. +- [ ] Review GitHub issues against the frozen scope. Record that issue #416's + original gate-coupling problem is resolved; keep issue #35 and other + new-feature requests outside the 1.0 milestone. +- [ ] Require optional runner upgrades or other infrastructure changes only when + they fix a demonstrated release failure. + +## Chunk 5: Release candidate and final gate + +Mark these complete for the exact candidate, even where an earlier commit passed +an equivalent check. Re-run affected checks after any candidate change. + +- [ ] Publish or distribute `1.0.0rc1` after correctness and human reviews pass. +- [ ] Build the sdist and wheel from the candidate tag with its actual version. +- [ ] Inspect artifact contents: version, public modules, `py.typed`, license, + metadata, and required source-distribution files. +- [ ] Install and exercise the wheel and sdist in clean environments. Verify a + silent core-only import and documented workflows outside the source tree. +- [ ] Run current dependencies on Python 3.11, 3.12, 3.13, and 3.14 on Ubuntu, + Ubuntu ARM, macOS, and Windows. Preserve the current coverage policy. +- [ ] Run minimum dependencies on Ubuntu with Python 3.11 and 3.14. +- [ ] Pass JIT-enabled checks on Ubuntu and Windows, optional-dependency checks, + the clean-wheel check, and the manual scientific release tests. +- [ ] Run the full serial suite and `uvx nox -s lint` on the final source. +- [ ] Pass strict documentation, installed-wheel examples, all executable + notebooks, external link checking, and Read the Docs. +- [ ] Review remaining skips, xfails, warnings, approximations, and experimental + paths. Confirm their technical reasons and accurate user documentation. +- [ ] Record source commit and tag, dependency versions, commands, test and + documentation results, and artifact checksums in the release evidence. +- [ ] Complete Aaron's final review of artifacts and user-facing material. + Resolve every remaining software release blocker. +- [ ] Tag and publish 1.0, archive the exact published source and artifacts, and + complete citation metadata with the archive identifier. + +For memory and process-tensor checks, use serial execution and capped numerical +threads when needed: + +```bash +OPENBLAS_NUM_THREADS=1 \ +OMP_NUM_THREADS=1 \ +MKL_NUM_THREADS=1 \ +NUMEXPR_NUM_THREADS=1 \ +NUMBA_NUM_THREADS=1 \ +uv run pytest -n 0 -p no:cacheprovider tests/characterization/memory tests/test_memory_characterizer.py +``` + +## Optional cleanup + +These tasks do not block 1.0 unless they expose a correctness, reliability, or +resource-use defect. + +- [x] Consolidate duplicate analog and digital golden tests while preserving + independent references, observable ordering, and actual pool coverage. +- [x] Reduce redundant shot sampling or flaky random assertions using tests of + the intended probability or measurement contract. +- [ ] Shorten the quickstart and move advanced material into focused guides + where that improves first use. +- [ ] Reduce optional documentation dependency cost if the build is needlessly + expensive. Preserve coverage of supported optional paths. +- [x] Record slowest-test durations and investigate avoidable memory use. Keep + scientific regression tests whose cost protects meaningful behavior. + +## SciPost paper evidence + +These tasks are required for the paper and its numerical claims. They do not +block the software release unless the release makes the same unsupported claim. +Archive data without adding package persistence or uncertainty APIs. + +- [ ] Select paper examples, benchmarks, figures, and claims supported by the + frozen existing feature set. +- [ ] Archive scripts, inputs, probe settings, intervention schedules, seeds, + raw outputs, and plotting scripts for every quantitative claim. +- [ ] Record the YAQS commit and tag, dependency lock or container, Python + version, OS, hardware, commands, metrics, and tolerances. +- [ ] Add checksums for each paper input, result, and figure. Store numerical + arrays with readable metadata rather than relying on saved Python objects. +- [ ] Reproduce every paper figure and numerical table in a clean environment + using archived inputs and the published software artifact. +- [ ] Report actual trajectory counts and statistical errors from archived raw + trajectories in the paper analysis. +- [ ] Complete and archive the response-matrix campaign if the paper uses it. + Keep unresolved scientific discrepancies explicit. +- [ ] Finalize the paper's software citation and archive links separately from + citations to the underlying methods. + +## Deferred features and refactoring + +The following work is outside 1.0: + +- HDF5 persistence, a versioned result-file schema, or a new save/load API. +- Early stopping for stochastic trajectories. +- A circuit statevector backend. +- Neural-network noise characterization. +- A new process-tensor compression algorithm. +- Scheduled or trajectory-averaged `Observable(MPO)` support. +- Converting local observables, diagnostics, or bitstrings into MPOs. +- A new uncertainty-reporting `Result` API. +- Multi-level or qudit simulation features. +- Trajectory visualization. +- Broad rewrites based only on module size and a package-wide validation + reorganization. +- Compatibility adapters for abandoned pre-1.0 APIs. diff --git a/docs/index.md b/docs/index.md index 63381ac4b..5100f49e0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -107,8 +107,8 @@ Simulator configuration :titlesonly: Analog simulation -Circuit simulation -Circuit measurements +Digital (circuit) simulation +Shot-based simulation Analog-digital simulation ``` @@ -118,8 +118,8 @@ Analog-digital simulation :maxdepth: 1 :titlesonly: -Environmental memory -Noise characterization +Environmental memory characterization +Noise model characterization Circuit verification ``` @@ -129,12 +129,12 @@ Circuit verification :maxdepth: 1 :titlesonly: -Ensembles and correlations +Ensemble evolution Scheduled jumps Custom gates -Transmon emulation +Superconducting qubit (transmon) emulation Trapped-ion emulation -Surrogate models (experimental) +Non-Markovian transformer models (experimental) ``` From e6f033e9b389b56362c6b6790947434328dd648a Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:41:19 +0200 Subject: [PATCH 15/30] updated transmon example --- docs/examples/hamiltonians.md | 3 +- docs/examples/transmon_emulation.md | 375 +++++++++++++++++----------- docs/index.md | 2 +- 3 files changed, 227 insertions(+), 153 deletions(-) diff --git a/docs/examples/hamiltonians.md b/docs/examples/hamiltonians.md index 0992ac3e7..14784f470 100644 --- a/docs/examples/hamiltonians.md +++ b/docs/examples/hamiltonians.md @@ -394,7 +394,8 @@ H_transmon = Hamiltonian.coupled_transmon( ) ``` -A full SWAP-style open-system example is in {doc}`transmon_emulation`. +For excitation transfer through a resonator, including relaxation and dephasing, +see {doc}`transmon_emulation`. ## Trapped-ion position grid diff --git a/docs/examples/transmon_emulation.md b/docs/examples/transmon_emulation.md index 3a66dfbb9..cd04b5990 100644 --- a/docs/examples/transmon_emulation.md +++ b/docs/examples/transmon_emulation.md @@ -2,201 +2,274 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true execution_timeout: 300 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - -# Transmon-Resonator Chain Emulation - -This example simulates a **qubit–resonator–qubit** chain with -{meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.coupled_transmon` -(dipole coupling per -{meth}`~mqt.yaqs.core.data_structures.mpo.MPO.coupled_transmon`). +# Superconducting Qubit Emulation -We prepare $|100\rangle$ (left transmon excited) and evolve for one resonant -swap period $T_{\mathrm{swap}} = \pi/(\sqrt{2}\,g)$. The same evolution is run -**twice**: +A resonator can carry an excitation between superconducting qubits. How much +reaches the receiving qubit, and how does noise affect the transfer? We model +two transmons coupled through a resonator, follow their populations, and compare +several relaxation and dephasing strengths. Keeping a third transmon level also +lets us track occupation outside the qubit computational subspace. -1. **Noiseless** — unitary analog simulation (TDVP on the MPO). -2. **Noisy** — open-system simulation with relaxation and dephasing on the qubit - sites (TJM trajectories). +This guide uses the standard YAQS installation and Matplotlib. Run the cells in +order in a notebook. For a script, use the entry-point guard in +{doc}`simulator_initialization`. -Local projectors track the $|1\rangle$ population on each transmon and the -$|2\rangle$ population on every site. Binary bitstring observables and shot -counts are restricted to all-qubit states, so this non-qubit example uses local -three-level observables instead. The sum of the local $|2\rangle$ populations is -the expected number of sites in the leakage level. +## 1. Build the transmon–resonator model -## 1. Hamiltonian and initial state +`Hamiltonian.coupled_transmon` places transmons at even sites and resonators at +odd sites. Here, sites 0 and 2 are three-level transmons, and site 1 is a +four-level resonator. Each transmon has a Duffing term +$\omega_q n+\alpha n(n-1)/2$, while the resonator has energy $\omega_r n$. +Neighboring sites interact through the full dipole coupling +$g(b+b^\dagger)(a+a^\dagger)$, where $b$ and $a$ lower the transmon and +resonator levels. -```{code-cell} ipython3 +```{code-cell} python import numpy as np -from mqt.yaqs import Hamiltonian, State -length = 3 # qubit – resonator – qubit +from mqt.yaqs import Hamiltonian + qubit_dim = 3 -resonator_dim = 3 -w_q = 4 / (2 * np.pi) -w_r = 4 / (2 * np.pi) -alpha = -0.3 / (2 * np.pi) -g = 0.2 / (2 * np.pi) - -H_0 = Hamiltonian.coupled_transmon( - length=length, +resonator_dim = 4 +physical_dimensions = [qubit_dim, resonator_dim, qubit_dim] +coupling = 1.0 +hamiltonian = Hamiltonian.coupled_transmon( + length=3, qubit_dim=qubit_dim, resonator_dim=resonator_dim, - qubit_freq=w_q, - resonator_freq=w_r, - anharmonicity=alpha, - coupling=g, + qubit_freq=20.0 * coupling, + resonator_freq=20.0 * coupling, + anharmonicity=-1.5 * coupling, + coupling=coupling, ) +transfer_time = np.pi / (np.sqrt(2) * coupling) +``` -T_swap = np.pi / (np.sqrt(2) * g) -dt = T_swap / 100 +We use $\hbar=1$ and measure frequencies and rates in units of $g$, so time is +in units of $1/g$. The frequency arguments enter the Hamiltonian directly: +convert ordinary frequencies to angular frequencies before supplying dimensional +values. YAQS adds no factor of $2\pi$. + +On resonance, $T=\pi/(\sqrt{2}g)$ estimates the first complete transfer in the +rotating-wave approximation. The factory retains counter-rotating terms, so +transfer at this time is approximate and total excitation is not exactly +conserved. This is a model of excitation transfer; one prepared state does not +validate a SWAP gate on arbitrary inputs. + +## 2. Excite the left transmon + +Start in $|100\rangle$: the left transmon is excited, while the resonator and +right transmon start in their ground states. Characters in `basis_string` follow +site order, starting at site 0. + +```{code-cell} python +from mqt.yaqs import State -# |100⟩: left qubit (site 0) in |1⟩ state = State( - length, - initial="basis", - basis_string="100", - physical_dimensions=[qubit_dim, resonator_dim, qubit_dim], + 3, initial="basis", basis_string="100", physical_dimensions=physical_dimensions, ) ``` -## 2. Observables and shared parameters +The explicit dimensions must match the Hamiltonian. YAQS uses an MPS by default +and supports different local dimensions within the same chain. + +## 3. Choose populations and the time grid -```{code-cell} ipython3 +Use local matrix observables to measure each site's $|1\rangle$ population. On +the transmons, also measure $|2\rangle$ population to detect leakage from the +computational subspace. Mean occupation $\langle n\rangle$ on all three sites +lets us track total excitation. + +```{code-cell} python from mqt.yaqs import AnalogSimParams, Observable -projector_1 = np.diag([0.0, 1.0, 0.0]) -projector_2 = np.diag([0.0, 0.0, 1.0]) -population_observables = [ - Observable(projector_1, sites=0), - Observable(projector_1, sites=2), - *(Observable(projector_2, sites=site) for site in range(length)), +observables = [ + Observable(np.diag(np.arange(dim) == 1).astype(float), site) + for site, dim in enumerate(physical_dimensions) ] - -sim_params = AnalogSimParams( - observables=population_observables, - elapsed_time=T_swap, - dt=dt, - sample_timesteps=True, +observables += [Observable(np.diag([0.0, 0.0, 1.0]), site) for site in (0, 2)] +observables += [ + Observable(np.diag(np.arange(dim)).astype(float), site) + for site, dim in enumerate(physical_dimensions) +] +params = AnalogSimParams( + observables=observables, + elapsed_time=transfer_time, + dt=transfer_time / 80, + order=2, + num_traj=32, + preset="balanced", + random_seed=7, ) +``` +Each matrix matches its site's dimension. The observable order is three +$|1\rangle$ populations, two transmon $|2\rangle$ populations, then three mean +occupations. Binary bitstring observables and shot counts require an all-qubit +state; local matrix observables also work with higher levels. -def population_curve(result, observable_index: int) -> np.ndarray: - values = result.expectation_values[observable_index] - if values is None: - msg = f"observable {observable_index} has no values" - raise ValueError(msg) - return np.asarray(values, dtype=float) - +The grid contains 81 samples over one transfer interval. The `balanced` preset +sets numerical tolerances; `order=2` selects second-order TJM for noisy runs. +Each noisy calculation averages 32 trajectories. Sampling error, timestep error, +and the chosen level cutoffs need separate convergence checks. -def leakage_curve(result) -> np.ndarray: - return sum((population_curve(result, index) for index in range(2, 5)), start=np.zeros(len(result.times))) -``` +## 4. Follow the noiseless transfer -## 3. Noiseless SWAP - -```{code-cell} ipython3 -import copy +Initialize the simulator separately and omit a noise model for the baseline. +YAQS preserves the input state, so subsequent runs can reuse the same +preparation. +```{code-cell} python from mqt.yaqs import Simulator -sim = Simulator(show_progress=False) -result_clean = sim.run(copy.deepcopy(state), H_0, copy.deepcopy(sim_params)) +simulator = Simulator(show_progress=False) +noiseless = simulator.run(state, hamiltonian, params) +times = noiseless.times +values = np.asarray(noiseless.expectation_values) +print(f"Right transmon population at T: {values[2, -1]:.3f}") ``` -```{code-cell} ipython3 -left_clean = population_curve(result_clean, 0) -right_clean = population_curve(result_clean, 1) -times = sim_params.times -``` +`values` has shape `(8, 81)`: observables by sampled times. The first three rows +show the excitation moving through the chain. Rows 3 and 4 measure leakage on +the left and right transmons. Their sum is the expected number of transmons in +$|2\rangle$, rather than the probability that either transmon has leaked. The +resonator's higher photon states are not qubit leakage. -## 4. Noisy SWAP - -Relaxation and dephasing on transmon sites (even indices). Built-in `lowering` -and `pauli_z` processes are 2×2; for `qubit_dim = 3` we pass explicit jump -matrices ({class}`~mqt.yaqs.core.libraries.gate_library.Destroy` and a -computational-subspace dephasing operator). For log-normal and other distributed -noise strengths, see {doc}`realistic_noise_models`. - -```{code-cell} ipython3 -from mqt.yaqs import NoiseModel -from mqt.yaqs.core.libraries.gate_library import Destroy +```{code-cell} python +:tags: [hide-input] +import matplotlib.pyplot as plt +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", "font.serif": ["STIXGeneral"], "mathtext.fontset": "stix", + "font.size": 10, "axes.labelsize": 11, "axes.linewidth": 0.8, + "xtick.direction": "in", "ytick.direction": "in", "svg.fonttype": "none", + "legend.frameon": False, +}) +scaled_times = times / transfer_time +site_labels = ["Left transmon", "Resonator", "Right transmon"] +site_colors = ["#D55E00", "0.5", "#0072B2"] +fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9), layout="constrained") +for site, label, color in zip(range(3), site_labels, site_colors, strict=True): + axes[0].plot(scaled_times, values[site], color=color, linewidth=1.8, label=label) +for row, label, color in ((3, "Left transmon", site_colors[0]), (4, "Right transmon", site_colors[2])): + axes[1].plot(scaled_times, 1e3 * values[row], color=color, linewidth=1.4, label=label) +axes[0].set(ylabel=r"$|1\rangle$ population", ylim=(0, 1.05)) +axes[1].set(ylabel=r"$|2\rangle$ population ($10^{-3}$)") +for ax, title in zip(axes, ["(a) Excitation transfer", "(b) Transmon leakage"], strict=True): + ax.set(xlabel=r"Time $t/T$", xlim=(0, 1)) + ax.set_title(title, loc="left", fontsize=11) + ax.legend(fontsize=8) +plt.show() +``` -relax = Destroy(qubit_dim).matrix -dephase = np.diag([1.0, -1.0, 1.0]).astype(complex) # |2⟩ unaffected +**The resonator mediates transfer between the transmons.** The resonator +population rises and falls while the receiving transmon approaches unit +population near $T$. Small, rapid excursions into $|2\rangle$ remain visible on +the separate linear scale. The full dipole interaction permits these excursions +even from a single-excitation preparation. The plotted receiver population does +not measure the fidelity of an arbitrary transferred quantum state. -noise_model = NoiseModel( - [{"name": "t1", "sites": [i], "strength": 0.03, "matrix": relax} for i in (0, 2)] - + [{"name": "dephase", "sites": [i], "strength": 0.02, "matrix": dephase} for i in (0, 2)] -) +## 5. Add multilevel relaxation and dephasing -noisy_params = AnalogSimParams( - observables=population_observables, - elapsed_time=T_swap, - dt=dt, - sample_timesteps=True, - num_traj=32, - random_seed=7, -) +The built-in `lowering` and `pauli_z` channels are two-dimensional. For a +three-level transmon, supply explicit matrices. The annihilation matrix $b$ +relaxes $|1\rangle$ to $|0\rangle$ and $|2\rangle$ to $|1\rangle$, with the +oscillator's $\sqrt{2}$ matrix element. The number operator $n$ produces pure +dephasing without directly changing populations. -result_noisy = sim.run(copy.deepcopy(state), H_0, noisy_params, noise_model) -``` +```{code-cell} python +from mqt.yaqs import NoiseModel -```{code-cell} ipython3 -left_noisy = population_curve(result_noisy, 0) -right_noisy = population_curve(result_noisy, 1) +lowering = np.diag(np.sqrt(np.arange(1, qubit_dim)), k=1) +number = np.diag(np.arange(qubit_dim)).astype(float) +relaxation_rates = coupling * np.array([0.05, 0.15, 0.6]) +results = {0.0: noiseless} +for rate in relaxation_rates: + noise = NoiseModel( + [{"name": "relaxation", "sites": [site], "strength": rate, "matrix": lowering} + for site in (0, 2)] + + [{"name": "dephasing", "sites": [site], "strength": 2 * rate, "matrix": number} + for site in (0, 2)] + ) + results[rate] = simulator.run(state, hamiltonian, params, noise) ``` -## 5. Comparison plot - -```{code-cell} ipython3 ---- -mystnb: - image: - width: 90% - align: center ---- -import matplotlib.pyplot as plt - -fig, (ax_pop, ax_leak) = plt.subplots(1, 2, figsize=(9, 3.5)) - -ax_pop.plot(times, right_clean, "-", color="tab:blue", label=r"noiseless right $P(|1\rangle)$") -ax_pop.plot(times, left_clean, "-", color="tab:orange", label=r"noiseless left $P(|1\rangle)$") -ax_pop.plot(times, right_noisy, "--", color="tab:blue", label=r"noisy right $P(|1\rangle)$") -ax_pop.plot(times, left_noisy, "--", color="tab:orange", label=r"noisy left $P(|1\rangle)$") -ax_pop.axvline(T_swap, color="gray", linestyle=":", alpha=0.6, label=r"$T_{\mathrm{swap}}$") -ax_pop.set_xlabel("time") -ax_pop.set_ylabel("probability") -ax_pop.set_title("SWAP populations: noiseless vs noisy") -ax_pop.legend(fontsize=8) -ax_pop.grid(alpha=0.3) - -leak_clean = leakage_curve(result_clean) -leak_noisy = leakage_curve(result_noisy) -ax_leak.plot(times, leak_clean, "-", color="tab:green", label="noiseless leakage") -ax_leak.plot(times, leak_noisy, "--", color="tab:red", label="noisy leakage") -ax_leak.set_xlabel("time") -ax_leak.set_ylabel(r"summed $|2\rangle$ population") -ax_leak.set_title("Occupation of the leakage level") -ax_leak.legend(fontsize=8) -ax_leak.grid(alpha=0.3) - -plt.tight_layout() +`strength` is a Lindblad rate: YAQS multiplies each supplied matrix by +`sqrt(strength)`. Here the jumps are $\sqrt{\gamma}\,b$ and $\sqrt{2\gamma}\,n$. +For an isolated transmon's $|0\rangle$–$|1\rangle$ transition, the relaxation +and pure-dephasing times are both $1/\gamma$. The sweep increases both channels +together and leaves the resonator noise-free. These deliberately short coherence +times make the competition with transfer visible; the parameters are +illustrative, rather than a fit to a device. + +Parallel execution is enabled by default. The documentation suppresses progress +bars with `show_progress=False`; omit that setting to see progress. The seed +fixes random streams for repeated runs with the same configuration. + +```{code-cell} python +:tags: [hide-input] +from matplotlib.colors import Normalize + +colors = ["0.2", "#56B4E9", "#0072B2", "#D55E00"] +fig, axes = plt.subplots(3, 2, figsize=(7.2, 6.6), layout="constrained") +for index, (ax, (rate, result)) in enumerate(zip(axes[:2].flat, results.items(), strict=True)): + means = np.asarray(result.expectation_values) + image = ax.pcolormesh(scaled_times, np.arange(3), means[:3], shading="auto", + cmap="cividis", norm=Normalize(0, 1), rasterized=True) + title = "(a) No noise" if index == 0 else rf"({chr(97 + index)}) $\gamma/g={rate / coupling:g}$" + ax.set(xlabel=r"Time $t/T$", yticks=[0, 1, 2], yticklabels=["Left", "Resonator", "Right"], xlim=(0, 1)) + ax.set_title(title, loc="left", fontsize=11) +fig.colorbar(image, ax=list(axes[:2].flat), label=r"$|1\rangle$ population", ticks=[0, 0.5, 1], shrink=0.8) + +for (rate, result), color in zip(results.items(), colors, strict=True): + means = np.asarray(result.expectation_values) + trajectories = np.asarray(result.trajectories) + label = "No noise" if rate == 0 else rf"$\gamma/g={rate / coupling:g}$" + for ax, mean, samples in ( + (axes[2, 0], means[2], trajectories[2]), + (axes[2, 1], means[5:8].sum(axis=0), trajectories[5:8].sum(axis=0)), + ): + ax.plot(scaled_times, mean, color=color, linewidth=1.8, label=label) + if samples.shape[0] > 1: + standard_error = samples.std(axis=0, ddof=1) / np.sqrt(samples.shape[0]) + ax.fill_between(scaled_times, mean - standard_error, mean + standard_error, color=color, alpha=0.15) +axes[2, 0].set(ylabel=r"Right transmon $|1\rangle$ population") +axes[2, 1].set(ylabel=r"Total excitation $\sum_i\langle n_i\rangle$") +for ax, title in zip(axes[2], ["(e) Received excitation", "(f) Excitation remaining"], strict=True): + ax.set(xlabel=r"Time $t/T$", xlim=(0, 1), ylim=(0, 1.08)) + ax.set_title(title, loc="left", fontsize=11) +axes[2, 0].legend(fontsize=8, loc="upper left") plt.show() ``` -## Related topics - -- {doc}`analog_simulation` — analog time evolution and noise models -- {doc}`realistic_noise_models` — distributed noise strengths -- {doc}`state_initialization` — custom `physical_dimensions` and basis states -- {doc}`simulation_parameters` — `sample_timesteps`, `num_traj`, and observables +**Stronger noise suppresses transfer to the right transmon.** Relaxation removes +excitation, while dephasing disrupts the coherent exchange through the +resonator. The lower panels separate received population from total excitation; +the latter can have small coherent excursions because of the counter-rotating +terms. All heatmaps use one color scale. Shading shows one standard error from +the trajectory samples, with sites summed within each trajectory before +estimating uncertainty in total excitation. These bands measure sampling +uncertainty, not numerical or level-cutoff error. + +## Further options + +Increase `num_traj` to reduce sampling fluctuations and refine `dt` and +numerical tolerances to check propagation accuracy. Increase `qubit_dim` and +`resonator_dim` separately to check level truncation, rebuilding the state, +observables, and jump matrices to match. Three transmon levels suffice to +illustrate leakage here; they do not establish convergence for a driven device. + +To add photon loss, supply a resonator-sized annihilation matrix at site 1. +Other custom channels and distributed strengths are described in +{doc}`realistic_noise_models`. See {doc}`hamiltonians` for longer alternating +chains, {doc}`state_initialization` for other preparations, and +{doc}`simulation_parameters` for accuracy and trajectory settings. diff --git a/docs/index.md b/docs/index.md index 5100f49e0..d2358dfa4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -63,7 +63,7 @@ flowchart LR | Compare scalable MPS, MCWF, and Lindblad analog paths | {doc}`examples/representation_comparison` | | Two-time correlations and typicality ensembles | {doc}`examples/ensemble_evolution` | | Scheduled jumps at fixed times | {doc}`examples/scheduled_jumps` | -| Transmon–resonator SWAP (noiseless vs noisy) | {doc}`examples/transmon_emulation` | +| Transfer an excitation between superconducting qubits | {doc}`examples/transmon_emulation` | | Static and moving trapped-ion position-grid dynamics | {doc}`examples/trapped_ion` | | Characterize environmental memory effects via probing the process | {doc}`examples/characterization` | | Study how long environmental memory persists in a system | {ref}`Memory persistence ` in {doc}`examples/characterization` | From d523d7b03a2b16e21bf5755ea25d6380fb0a0a45 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:32:13 +0200 Subject: [PATCH 16/30] cleaned up trapped ion example --- docs/examples/hamiltonians.md | 6 +- docs/examples/trapped_ion.md | 441 ++++++++++++++++++++-------------- docs/index.md | 4 +- 3 files changed, 269 insertions(+), 182 deletions(-) diff --git a/docs/examples/hamiltonians.md b/docs/examples/hamiltonians.md index 14784f470..ef36b5e80 100644 --- a/docs/examples/hamiltonians.md +++ b/docs/examples/hamiltonians.md @@ -397,7 +397,7 @@ H_transmon = Hamiltonian.coupled_transmon( For excitation transfer through a resonator, including relaxation and dephasing, see {doc}`transmon_emulation`. -## Trapped-ion position grid +## Trapped ion position grid {meth}`~mqt.yaqs.core.data_structures.mpo.MPO.trapped_ion` builds a **static** Hamiltonian for one or two ions on a uniform position grid. Each ion is one MPO @@ -440,8 +440,8 @@ H_pair = Hamiltonian.from_mpo( ``` Pair with {class}`~mqt.yaqs.core.data_structures.state.State` using -`physical_dimensions=[len(positions)]` per ion site. A wavepacket reflection -benchmark is in {doc}`trapped_ion`. +`physical_dimensions=[len(positions)]` per ion site. For wavepacket oscillation, +trap transport, and heating from random momentum kicks, see {doc}`trapped_ion`. ```{note} YAQS applies $\exp(-\mathrm{i}\,\Delta t\, H)$ during evolution. When using SI diff --git a/docs/examples/trapped_ion.md b/docs/examples/trapped_ion.md index ffe81dcc8..ab429da6a 100644 --- a/docs/examples/trapped_ion.md +++ b/docs/examples/trapped_ion.md @@ -2,228 +2,315 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true execution_timeout: 300 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` +# Trapped Ion Emulation -# Static and Moving Trapped-Ion Position-Grid Emulation +Moving a trapped ion changes its position, but can also leave it oscillating +after the trap stops. Random force impulses add another source of motion. We +first follow a displaced wavepacket in a fixed harmonic well, then move the well +and compare several noise strengths. Position distributions and motional energy +show how coherent transport excitation differs from heating. -This example evolves a **single ion** on a finite position grid with -{meth}`~mqt.yaqs.core.data_structures.mpo.MPO.trapped_ion`. Each ion is one MPO -site; the local Hilbert space is the grid itself. The Hamiltonian combines a -finite-difference kinetic term and a harmonic trap—see {doc}`hamiltonians` for -the factory API and two-ion Coulomb extensions. +This guide uses the standard YAQS installation and Matplotlib. Run the cells in +order in a notebook. For a script, use the entry-point guard in +{doc}`simulator_initialization`. -We first initialize a displaced harmonic-oscillator wavepacket in a static -central well. In the continuum limit, its center follows -$\langle x(t)\rangle = x_0 \cos(\omega t)$, so after half a trap period it -reaches the opposite turning point. +## 1. Build a harmonic trap on a position grid -## 1. Hamiltonian and initial state +Each ion occupies one MPO site, whose local basis consists of position-grid +points. `MPO.trapped_ion` combines a finite-difference kinetic operator with a +harmonic potential centered at `trap_center`. We use one ion and dimensionless +units with $\hbar=m=\omega=1$. Position is in oscillator-length units and time +is in units of $1/\omega$. -```{code-cell} ipython3 +```{code-cell} python import numpy as np -from mqt.yaqs import Hamiltonian, MPO, Observable, State +from mqt.yaqs import Hamiltonian, MPO, State +positions = np.linspace(-8.0, 8.0, 65) +grid_dim = len(positions) +grid_spacing = positions[1] - positions[0] omega = 1.0 -initial_displacement = 1.0 -half_period = np.pi / omega -positions = np.linspace(-8.0, 8.0, 33) -grid_dim = len(positions) -initial_grid_state = np.exp(-0.5 * (positions - initial_displacement) ** 2).astype(np.complex128) -initial_grid_state /= np.linalg.norm(initial_grid_state) +def trap_at(center): + return Hamiltonian.from_mpo( + MPO.trapped_ion(positions, masses=[1.0], omega=omega, trap_center=center) + ) + -hamiltonian = Hamiltonian.from_mpo(MPO.trapped_ion(positions, masses=[1.0], omega=omega)) -state = State(length=1, vector=initial_grid_state, physical_dimensions=[grid_dim]) -position_observable = Observable("position", 0, positions=positions) -``` +def state_at(center): + packet = np.exp(-0.5 * (positions - center) ** 2).astype(complex) + packet /= np.linalg.norm(packet) + return State( + 1, tensors=[packet.reshape(grid_dim, 1, 1)], physical_dimensions=[grid_dim], + ) -## 2. Noiseless evolution to $T/2$ - -```{code-cell} ipython3 -from mqt.yaqs import AnalogSimParams, Simulator - -params = AnalogSimParams( - observables=[position_observable], - elapsed_time=half_period, - dt=half_period / 16, - max_bond_dim=None, - svd_threshold=1e-12, - krylov_tol=1e-12, - preset="exact", - get_state=True, - sample_timesteps=True, -) -result = Simulator(show_progress=False).run(state, hamiltonian, params) -final_state = result.output_state.vector -position_expectation = np.real(result.expectation_values[0]) -final_x = float(position_expectation[-1]) +static_hamiltonian = trap_at(0.0) +static_state = state_at(1.0) ``` -The position observable is a custom one-site matrix on the grid basis. The final -$\langle x\rangle$ is close to $-x_0$ but not exact because the simulation uses -a finite grid and a finite-difference kinetic operator. +The normalized Gaussian approximates a displaced oscillator ground state. Its +components are amplitudes on the finite grid, so their squared magnitudes sum to +one. A single MPS tensor has shape `(grid_dim, 1, 1)`: one physical index and +two bond indices. This representation supports both the fixed and moving +Hamiltonians below. Supplying `vector=` instead selects the MCWF backend, which +does not support piecewise Hamiltonians. -```{code-cell} ipython3 -print(f"Initial = {initial_displacement:.6f}") -print(f"Final at T/2 = {final_x:.6f}") -print(f"Continuum target = {-initial_displacement:.6f}") +## 2. Follow an oscillating wavepacket + +Measure the mean position and the population of every grid point. The latter +uses projectors $|x_j\rangle\langle x_j|$ and gives a position distribution at +each sampled time, without requesting a sequence of output states. + +```{code-cell} python +from mqt.yaqs import AnalogSimParams, Observable, Simulator + +position_observable = Observable("position", 0, positions=positions) +grid_projectors = [Observable(np.diag(row), 0) for row in np.eye(grid_dim)] +period = 2 * np.pi / omega +static_params = AnalogSimParams( + observables=[position_observable, *grid_projectors], + elapsed_time=period, + dt=period / 80, + preset="balanced", +) +simulator = Simulator(show_progress=False) +static_result = simulator.run(static_state, static_hamiltonian, static_params) +static_times = static_result.times +static_values = np.asarray(static_result.expectation_values) +static_density = static_values[1:] / grid_spacing ``` -## 3. Wavepacket over time +`expectation_values` follows the supplied observable order. Stacking these +arrays gives shape `(66, 81)`: the mean position, then 65 grid populations. +Divide populations by the grid spacing to plot probability density per unit +position. In the continuum, the mean follows $\langle x(t)\rangle=\cos t$. That +curve is a useful comparison; the finite-difference Hamiltonian differs slightly +from the continuum oscillator. -```{code-cell} ipython3 ---- -mystnb: - image: - width: 90% - align: center ---- +```{code-cell} python +:tags: [hide-input] import matplotlib.pyplot as plt - -dense_hamiltonian = hamiltonian.to_matrix() -eigenvalues, eigenvectors = np.linalg.eigh(dense_hamiltonian) -coefficients = eigenvectors.conj().T @ initial_grid_state -phases = np.exp(-1j * eigenvalues[:, None] * params.times[None, :]) -states = eigenvectors @ (coefficients[:, None] * phases) -probability_density = np.abs(states) ** 2 - -fig, ax = plt.subplots(figsize=(7.2, 3.6), layout="constrained") -image = ax.imshow( - probability_density, - aspect="auto", - origin="lower", - extent=(params.times[0], params.times[-1], positions[0], positions[-1]), - cmap="viridis", +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", "font.serif": ["STIXGeneral"], "mathtext.fontset": "stix", + "font.size": 10, "axes.labelsize": 11, "axes.linewidth": 0.8, + "xtick.direction": "in", "ytick.direction": "in", "svg.fonttype": "none", + "legend.frameon": False, +}) +fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9), layout="constrained") +image = axes[0].pcolormesh( + static_times, positions, static_density, shading="auto", cmap="cividis", + vmin=0, vmax=0.6, rasterized=True, ) -ax.plot(params.times, position_expectation, color="white", lw=1.4, label=r"$\langle x\rangle$") -ax.set_xlabel(r"$t$") -ax.set_ylabel(r"$x$") -ax.set_title("Position-grid wavepacket density") -ax.legend(loc="upper right") -fig.colorbar(image, ax=ax, label=r"$|\psi(x,t)|^2$") +axes[0].plot(static_times, static_values[0], color="white", linewidth=1.3) +axes[0].set(ylabel=r"Position $x$", ylim=(-3, 3)) +axes[0].set_title("(a) Position distribution and mean", loc="left", fontsize=11) +fig.colorbar(image, ax=axes[0], label="Probability density", shrink=0.85) +axes[1].plot(static_times, static_values[0], color="#0072B2", linewidth=1.8, label="YAQS") +axes[1].plot(static_times, np.cos(static_times), color="0.4", linestyle="--", label="Continuum") +axes[1].set(ylabel=r"Mean position $\langle x\rangle$", ylim=(-1.15, 1.15)) +axes[1].set_title("(b) Oscillation in a fixed well", loc="left", fontsize=11) +axes[1].legend(fontsize=9) +for ax in axes: + ax.set(xlabel=r"Time $t$", xlim=(0, period), xticks=[0, np.pi, 2 * np.pi], + xticklabels=["0", r"$\pi$", r"$2\pi$"]) plt.show() ``` -## 4. Transport in a moving harmonic well +**The packet oscillates about the trap center.** Its mean nearly follows the +continuum curve over one period. The heatmap contains the actual YAQS grid +populations; the white line marks their mean. Small changes in shape and phase +reflect the finite grid and its kinetic operator. -A moving trap is a piecewise Hamiltonian: one static well per ``dt`` interval, -then a hold at the target. The trap center is a staircase from -$q\nobreak=\nobreak-1$ to $q\nobreak=\nobreak1$, constant on each analog step. +## 3. Move the well, then hold it fixed -```{code-cell} ipython3 -transport_positions = np.linspace(-6.0, 6.0, 25) -transport_grid_dim = len(transport_positions) +Start a new packet at $q=-1$ and translate the well to $q=1$ over four time +units. The control is a staircase: each trap center remains fixed for `dt=0.1`, +followed by a four-unit hold at the target. This deliberately simple protocol +leaves enough residual motion to see in the position distribution. + +```{code-cell} python start_center = -1.0 target_center = 1.0 -transport_duration = 10.0 -hold_duration = 5.0 -dt = 0.25 - - -def trap_at(trap_center: float) -> Hamiltonian: - return Hamiltonian.from_mpo( - MPO.trapped_ion( - transport_positions, - masses=[1.0], - omega=omega, - trap_center=trap_center, - ) - ) - - +transport_duration = 4.0 +hold_duration = 4.0 +dt = 0.1 n_transport = round(transport_duration / dt) -transport_pieces = [ - (trap_at(start_center + (target_center - start_center) * (step / n_transport)), dt) - for step in range(n_transport) -] +transport_centers = np.linspace(start_center, target_center, n_transport, endpoint=False) +target_hamiltonian = trap_at(target_center) moving_hamiltonian = Hamiltonian.piecewise([ - *transport_pieces, - (trap_at(target_center), hold_duration), + *[(trap_at(center), dt) for center in transport_centers], + (target_hamiltonian, hold_duration), ]) +transport_state = state_at(start_center) +``` -transport_wavepacket = np.exp(-0.5 * (transport_positions - start_center) ** 2).astype(np.complex128) -transport_wavepacket /= np.linalg.norm(transport_wavepacket) -transport_state = State( - length=1, - tensors=[transport_wavepacket.reshape(transport_grid_dim, 1, 1)], - physical_dimensions=[transport_grid_dim], -) -transport_position = Observable("position", 0, positions=transport_positions) +`Hamiltonian.piecewise` selects the well for each interval. Piece durations must +be integer multiples of the simulation timestep, and their sum must equal +`elapsed_time`. Piecewise evolution currently requires an MPS and TDVP, which +are used here. Reducing `dt` while rebuilding the staircase also changes the +control waveform; it is a separate check from refining the spatial grid. + +## 4. Measure residual motion and heating + +Alongside the mean and grid populations, measure $\langle x^2\rangle$ and the +energy of the final well. The ensemble position width is +$\sigma_x=\sqrt{\langle x^2\rangle-\langle x\rangle^2}$. During the hold, the +final-well energy measures motion left by transport and added by noise. + +```{code-cell} python transport_params = AnalogSimParams( - observables=[transport_position], + observables=[ + position_observable, + Observable(np.diag(positions**2), 0), + Observable(target_hamiltonian.to_matrix(), 0), + *grid_projectors, + ], elapsed_time=transport_duration + hold_duration, dt=dt, - tdvp_sweeps=2, - max_bond_dim=None, - svd_threshold=1e-12, - krylov_tol=1e-12, - preset="exact", - sample_timesteps=True, + order=2, + num_traj=32, + preset="balanced", + random_seed=7, ) +noiseless = simulator.run(transport_state, moving_hamiltonian, transport_params) +times = noiseless.times +``` -transport_result = Simulator(parallel=False, show_progress=False).run( - transport_state, - moving_hamiltonian, - transport_params, -) -transport_expectation = np.real(transport_result.expectation_values[0]) -transport_centers = [ - start_center + (target_center - start_center) * (step / n_transport) for step in range(n_transport) -] -n_hold_times = len(transport_params.times) - n_transport -scheduled_centers = np.asarray([*transport_centers, *([target_center] * n_hold_times)]) -hold_mask = transport_params.times >= transport_duration -residual_motion = np.max(np.abs(transport_expectation[hold_mask] - target_center)) -assert residual_motion > 0.1 -print(f"Maximum displacement from the target during the hold: {residual_motion:.3f}") +The observable arrays have shape `(68, 81)`: mean position, mean squared +position, final-well energy, then grid populations. The `balanced` preset sets +numerical tolerances. Second-order TJM averages 32 trajectories for each noisy +run below; the noiseless baseline needs only one trajectory. + +## 5. Add random momentum kicks + +Model random force impulses as momentum kicks in either direction. Multiplying +the wavefunction by $e^{\pm i\kappa x}$ shifts its momentum by $\pm\kappa$ in +our units. The two custom jumps are $L_\pm=\sqrt{\gamma/2}\,e^{\pm i\kappa X}$, +where $X=\operatorname{diag}(x_j)$ and $\kappa=1$. Equal rates give no preferred +direction. The kicks heat the ion without friction or thermal relaxation. + +```{code-cell} python +from mqt.yaqs import NoiseModel + +noise_rates = [0.1, 0.4, 1.0] +kick_size = 1.0 +results = {0.0: noiseless} +for rate in noise_rates: + noise = NoiseModel([ + {"name": "momentum_kick", "sites": [0], "strength": rate / 2, + "matrix": np.diag(np.exp(1j * sign * kick_size * positions))} + for sign in (-1, 1) + ]) + results[rate] = simulator.run(transport_state, moving_hamiltonian, transport_params, noise) ``` -Each transport interval uses a fixed trap center. During the hold the well stays -at the target while the ion continues to move. - -```{code-cell} ipython3 -fig, ax = plt.subplots(figsize=(7.2, 3.4), layout="constrained") -ax.step( - transport_params.times, - scheduled_centers, - where="post", - linestyle="--", - label=r"trap center $q(t)$", -) -ax.plot(transport_params.times, transport_expectation, label=r"ion $\langle x(t)\rangle$") -ax.axvline(transport_duration, color="0.6", ls=":", label="end of transport") -ax.set_xlabel(r"$t$") -ax.set_ylabel(r"$x$") -ax.set_title("Residual motion after linear trap transport") -ax.legend() +YAQS multiplies each supplied matrix by `sqrt(strength)`. Each direction has +rate $\gamma/2$, so $\gamma$ is the total kick rate in oscillator units. The +noise acts throughout transport and the hold, while the initial state, control +waveform, time grid, and numerical settings stay fixed. The stronger rates make +spreading visible over this short protocol. The grid extends to $x=\pm8$ to +leave room for the heated packet. + +Parallel execution is enabled by default. The documentation suppresses progress +bars with `show_progress=False`; omit that setting to see progress. The seed +fixes random streams for the same configuration, and each run preserves the +input state. + +```{code-cell} python +:tags: [hide-input] +from matplotlib.colors import Normalize + +colors = ["0.2", "#56B4E9", "#0072B2", "#D55E00"] +n_hold = round(hold_duration / dt) +scheduled_centers = np.r_[transport_centers, np.full(n_hold + 1, target_center)] +hold_mask = times >= transport_duration +fig, axes = plt.subplots(3, 2, figsize=(7.2, 7.1), layout="constrained") +for index, (ax, (rate, result)) in enumerate(zip(axes[:2].flat, results.items(), strict=True)): + means = np.asarray(result.expectation_values) + density = means[3:] / grid_spacing + image = ax.pcolormesh(times, positions, density, shading="auto", cmap="cividis", + norm=Normalize(0, 0.6), rasterized=True) + ax.step(times, scheduled_centers, where="post", color="white", linestyle="--", + linewidth=1.1, label="Trap center") + ax.plot(times, means[0], color="#E69F00", linewidth=1.2, label="Mean position") + title = "(a) No noise" if index == 0 else rf"({chr(97 + index)}) $\gamma={rate:g}$" + ax.set(xlabel=r"Time $t$", ylabel=r"Position $x$", xlim=(0, times[-1]), ylim=(-8, 8)) + ax.set_title(title, loc="left", fontsize=11) + if index == 0: + ax.legend(loc="upper left", fontsize=8, labelcolor="white") +fig.colorbar(image, ax=list(axes[:2].flat), label="Probability density", shrink=0.8) + +for (rate, result), color in zip(results.items(), colors, strict=True): + means = np.asarray(result.expectation_values) + samples = result.trajectories[2] + energy_se = samples.std(axis=0, ddof=1) / np.sqrt(len(samples)) if len(samples) > 1 else np.zeros_like(times) + width = np.sqrt(means[1] - means[0] ** 2) + label = "No noise" if rate == 0 else rf"$\gamma={rate:g}$" + axes[2, 0].plot(times, width, color=color, linewidth=1.7, label=label) + axes[2, 1].plot(times[hold_mask], means[2, hold_mask], color=color, linewidth=1.7) + axes[2, 1].fill_between(times[hold_mask], (means[2] - energy_se)[hold_mask], + (means[2] + energy_se)[hold_mask], color=color, alpha=0.15, linewidth=0) +axes[2, 0].axvline(transport_duration, color="0.5", linestyle=":", linewidth=1) +axes[2, 0].set(xlabel=r"Time $t$", ylabel=r"Position width $\sigma_x$", xlim=(0, times[-1])) +axes[2, 0].set_title("(e) Ensemble position spread", loc="left", fontsize=11) +axes[2, 0].legend(fontsize=8, ncol=2) +axes[2, 1].set(xlabel=r"Time $t$", ylabel=r"Energy $\langle H(q=1)\rangle$", + xlim=(transport_duration, times[-1])) +axes[2, 1].set_title("(f) Motional energy during the hold", loc="left", fontsize=11) plt.show() ``` -The ion does not end at rest: during the hold, $q(t)$ stays at the target while -$\langle x(t)\rangle$ oscillates around it. This staircase protocol is -intentionally idealized: the trap center is constant on each ``dt`` interval and -jumps at interval boundaries, so the ion is kicked non-adiabatically. Residual -motion can degrade later operations; smooth ramps reduce that error. - -This example uses dimensionless units with $\hbar=m=\omega=1$. For dimensional -inputs, use compatible time and energy units. A finer grid, smaller `dt`, and -slower trajectory reduce spatial and non-adiabatic transport errors. - -## Related topics - -- {doc}`hamiltonians` — `MPO.trapped_ion` parameters and two-ion Coulomb - channels -- {doc}`transmon_emulation` — another mixed-dimensional hardware model -- {doc}`analog_simulation` — analog time evolution and noise models -- {doc}`state_initialization` — custom `physical_dimensions` and manual vectors +**Transport leaves a coherent oscillation; random kicks broaden the packet and +raises its energy.** After $t=4$, the dashed trap center stays fixed while the +mean position continues to oscillate. Without noise, the packet remains narrow +and its energy stays constant during the hold. Stronger noise spreads the +position distribution and increases the motional energy. All heatmaps share one +color scale. + +The width in panel (e) describes the ensemble position distribution, including +variation between trajectories. It is not an error bar on the mean position. +Shading in panel (f) shows one standard error of the trajectory-averaged energy. +Thirty-two trajectories make the trend visible, but the curves retain sampling +fluctuations. Increase `num_traj` to resolve smaller differences. + +## 6. Adapt the model + +Check grid spacing and boundaries before interpreting a quantitative result. The +kinetic operator uses zero exterior boundary values, and a heated packet can +reach the edges. Refine the grid and enlarge its range separately. The continuum +Gaussian is also only an approximate ground state of the discrete Hamiltonian. +For dimensional inputs, use compatible units and supply a Hamiltonian divided by +$\hbar$ if your time unit requires it: YAQS evolves with $\exp(-iH\,dt)$. + +A slower or smoother transport protocol can reduce coherent residual motion. The +kick model isolates heating from random impulses. Other noise processes require +suitable jump operators; this model does not describe cooling. The factory +supports one or two ions, with a softened Coulomb interaction for two ions. It +describes motional dynamics on a position grid, rather than internal spin states +or a full laser-driven gate model. + +For a noiseless run, `get_state=True` also returns the final state. Noisy MPS +runs return ensemble observables and trajectory data instead of a single final +pure state. Grid projectors remain available for both cases, as shown above. + +## Related guides + +- {doc}`hamiltonians` — trap parameters, two-ion interactions, and piecewise + models. +- {doc}`analog_simulation` — noisy evolution, accuracy, and trajectory sampling. +- {doc}`state_initialization` — custom local dimensions and manual MPS tensors. +- {doc}`transmon_emulation` — excitation transfer in a multilevel hardware + model. diff --git a/docs/index.md b/docs/index.md index d2358dfa4..591d4065d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,7 +64,7 @@ flowchart LR | Two-time correlations and typicality ensembles | {doc}`examples/ensemble_evolution` | | Scheduled jumps at fixed times | {doc}`examples/scheduled_jumps` | | Transfer an excitation between superconducting qubits | {doc}`examples/transmon_emulation` | -| Static and moving trapped-ion position-grid dynamics | {doc}`examples/trapped_ion` | +| Transport a trapped ion and study motional noise | {doc}`examples/trapped_ion` | | Characterize environmental memory effects via probing the process | {doc}`examples/characterization` | | Study how long environmental memory persists in a system | {ref}`Memory persistence ` in {doc}`examples/characterization` | | Train a surrogate and predict how a system evolves under control sequences | {doc}`examples/memory_surrogate` | @@ -133,7 +133,7 @@ Ensemble evolution Scheduled jumps Custom gates Superconducting qubit (transmon) emulation -Trapped-ion emulation +Trapped ion emulation Non-Markovian transformer models (experimental) ``` From 1cabf3d65d9932ddef25aaaf17562eb2e1c5ddb8 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:00:05 +0200 Subject: [PATCH 17/30] added emulation --- docs/index.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 591d4065d..1acd0b8e4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -112,6 +112,16 @@ Shot-based simulation Analog-digital simulation ``` +```{toctree} +:caption: Emulation +:hidden: +:maxdepth: 1 +:titlesonly: + +Superconducting qubit (transmon) emulation +Trapped ion emulation +``` + ```{toctree} :caption: Characterization and verification :hidden: @@ -132,8 +142,6 @@ Circuit verification Ensemble evolution Scheduled jumps Custom gates -Superconducting qubit (transmon) emulation -Trapped ion emulation Non-Markovian transformer models (experimental) ``` From 571b0e5e29cbf64f6fe9dcc573d00227f59b6f0b Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:35:56 +0200 Subject: [PATCH 18/30] updated language --- docs/examples/ensemble_evolution.md | 374 +++++++++++++--------------- 1 file changed, 176 insertions(+), 198 deletions(-) diff --git a/docs/examples/ensemble_evolution.md b/docs/examples/ensemble_evolution.md index 9727006f3..71da36239 100644 --- a/docs/examples/ensemble_evolution.md +++ b/docs/examples/ensemble_evolution.md @@ -2,258 +2,232 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true execution_timeout: 600 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Ensemble Evolution -Use this page when you need **two-time correlators** (`multi_time_observables` -on -{class}`~mqt.yaqs.core.data_structures.simulation_parameters.AnalogSimParams`) -or **ensemble averages** over `list[State]` inputs—for dynamical typicality -studies, transport correlators, and finite-temperature observables estimated -from random pure states. +A two-time correlation follows how a measurement at a later time relates to an +operator applied to the initial state. Averaging these correlations over several +initial states helps study spin dynamics and transport. This guide starts with +one state, compares it with a small ensemble, and then follows local +spin-current correlations in a periodic chain. All evolution is unitary. + +This guide uses the standard YAQS installation and Matplotlib. Run the cells in +order in a notebook. For a script, use the entry-point guard in +{doc}`simulator_initialization`. -This page demonstrates workflows for computing two-time correlations in a -deterministic (noiseless, unitary) ensemble in YAQS. The focus is on compact, -executable examples: +## 1. Follow one state in an open spin chain -- Single-state auto/two-time correlations. -- Ensemble-averaged correlations (typicality view). -- Small periodic spin-current transport example. +Use six spin-$1/2$ sites with nearest-neighbor XXZ interactions and a transverse +field. With $S^\alpha=\sigma^\alpha/2$, the Hamiltonian is -## 1. Unitary analog evolution primer +```{math} +H = \sum_{r=0}^{L-2}\left[ +J_{xx}(S_r^x S_{r+1}^x+S_r^y S_{r+1}^y) ++\Delta S_r^z S_{r+1}^z\right] ++h_x\sum_{r=0}^{L-1}S_r^x. +``` -In unitary analog evolution, we have no noise or tensor jumps. Omit -`noise_model` in {meth}`~mqt.yaqs.Simulator.run` (it defaults to `None`). +`Hamiltonian.pauli` uses Pauli matrices, so two-spin coefficients include a +factor of $1/4$ and the field coefficient includes $1/2$. Set $J_{xx}=1$ and +$\hbar=1$, so time is in units of $1/J_{xx}$. -```{code-cell} ipython3 +```{code-cell} python import numpy as np -import matplotlib.pyplot as plt from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State -sim = Simulator(show_progress=False) -``` - -```{code-cell} ipython3 L = 6 Jxx = 1.0 delta = 0.7 h_x = 0.4 - -# Open XXZ + transverse field: H = Jxx ∑_r (S^x_r S^x_{r+1} + S^y_r S^y_{r+1}) + Δ ∑_r S^z_r S^z_{r+1} + h_x ∑_r S^x_r -# (Pauli convention in code: S^α = σ^α/2, matching two_body prefactors 0.25 * Jxx / Δ.) H_open = Hamiltonian.pauli( length=L, two_body=[(0.25 * Jxx, "X", "X"), (0.25 * Jxx, "Y", "Y"), (0.25 * delta, "Z", "Z")], one_body=[(0.5 * h_x, "X")], bc="open", ) - mid = L // 2 psi0 = State(L, initial="haar-random", pad=2) +sim = Simulator(show_progress=False) +``` + +The `haar-random` preset builds a random MPS from Haar-random isometries. Here, +`pad=2` limits its initial bond dimension to two. This is not a uniformly +sampled vector from the full Hilbert space. State initialization is unseeded, so +rerunning the notebook changes the numerical curves. Save the initial MPS +tensors when you need to reproduce a particular ensemble. + +First measure the central site's Pauli $Z$ expectation. `Observable("z", mid)` +represents $\sigma^z_m$; divide its expectation by two for $S^z_m$. +```{code-cell} python primer_params = AnalogSimParams( observables=[Observable("z", mid)], elapsed_time=5.1, dt=0.15, max_bond_dim=64, svd_threshold=1e-10, - sample_timesteps=True, ) - result_primer = sim.run(psi0, H_open, primer_params) -times_primer = primer_params.times +times_primer = result_primer.times zexp_primer = result_primer.expectation_values[0] ``` -```{code-cell} ipython3 -fig, ax = plt.subplots(1, 1, figsize=(5.4, 3.2)) -ax.plot(times_primer, zexp_primer, marker="o", ms=3) -ax.set_xlabel("t") -ax.set_ylabel(r"$\langle S^z_m(t)\rangle$") -ax.set_title("Single-state unitary evolution") -ax.grid(alpha=0.3) +```{code-cell} python +:tags: [hide-input] +import matplotlib.pyplot as plt +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", "font.serif": ["STIXGeneral"], "mathtext.fontset": "stix", + "font.size": 10, "axes.labelsize": 11, "axes.linewidth": 0.8, + "xtick.direction": "in", "ytick.direction": "in", "svg.fonttype": "none", + "legend.frameon": False, +}) +fig, ax = plt.subplots(figsize=(5.4, 2.8), layout="constrained") +ax.plot(times_primer, zexp_primer, color="#0072B2", linewidth=1.8) +ax.axhline(0, color="0.7", linewidth=0.6) +ax.set(xlabel=r"Time $t$", ylabel=r"$\langle\sigma^z_m(t)\rangle$", xlim=(0, 5.1)) +ax.set_title("Local spin dynamics", loc="left", fontsize=11) plt.show() ``` -## 2. Two-time Correlations - -For an initial state $|\psi(0)\rangle$ and unitary $U(t)$: +The local expectation changes even though the whole chain evolves unitarily. A +single curve depends on its initial state. Two-time correlations let us ask how +a specified initial perturbation affects the later dynamics. -- Autocorrelation (for one observable $O$): +## 2. Request two-time correlations - ```{math} - C_{OO}(t) = \langle \psi(0)| U^\dagger(t)\, O\, U(t)\, O |\psi(0)\rangle - ``` +For a state $|\psi_0\rangle$ and propagator $U(t)$, define -- Generic two-time correlation (probe $A$ and kick $B$): - - ```{math} - C_{AB}(t) = \langle \psi(0)| U^\dagger(t)\, A\, U(t)\, B |\psi(0)\rangle - ``` +```{math} +C_{AB}(t)=\langle\psi_0|U^\dagger(t)\,A\,U(t)\,B|\psi_0\rangle. +``` -These quantities probe dynamical memory and relaxation. They are standard -observables in **dynamical quantum typicality (DQT)** and related -finite-temperature dynamics studies, where one compares single-trajectory and -ensemble-averaged behavior. +Pass `(A, B)` in `multi_time_observables`: `B` acts at time zero and `A` is +measured at time $t$. Setting `A` and `B` equal gives an autocorrelation. The +product need not be Hermitian, so the result can be complex. -The unitary-ensemble backend computes `multi_time_observables` pairs for -`list[State]` inputs (each with `representation="mps"`, the default). -Autocorrelation is the special case where both the observables are the same -`(O, O)`. For a single-state demonstration, we pass a list with one element. +The correlation backend takes a `list[State]` of MPS inputs. A list with one +state gives a single-state result. Reuse `psi0` to connect the correlation +calculation with the local dynamics above. -```{code-cell} ipython3 +```{code-cell} python sz_mid = Observable("z", mid) sx_mid = Observable("x", mid) - single_state_params = AnalogSimParams( observables=[], elapsed_time=5.1, dt=0.15, max_bond_dim=64, svd_threshold=1e-10, - sample_timesteps=True, - multi_time_observables=[(sz_mid, sz_mid), (sz_mid, sx_mid)], # row 0: C_zz(t), row 1: C_zx(t) -) - -sim = Simulator(show_progress=False) -result_single = sim.run( - [State(L, initial="haar-random", pad=2)], H_open, single_state_params + multi_time_observables=[(sz_mid, sz_mid), (sz_mid, sx_mid)], ) - +result_single = sim.run([psi0], H_open, single_state_params) t_single = result_single.multi_time_times czz_single = result_single.multi_time_results[0] czx_single = result_single.multi_time_results[1] ``` -```{code-cell} ipython3 -fig, ax = plt.subplots(1, 1, figsize=(5.8, 3.4)) -ax.plot(t_single, np.real(czz_single), "o-", label=r"$C_{zz}(t)$") -ax.plot(t_single, np.real(czx_single), "s--", label=r"$C_{zx}(t)$") -ax.set_xlabel("t") -ax.set_ylabel(r"$C_{ab}(t)$") -ax.set_title("Single-state two-time correlations") -ax.legend() -ax.grid(alpha=0.3) -plt.show() -``` - -## 3. Typicality view: from one state to an ensemble +`multi_time_results` has shape `(2, 35)`: one row per pair, in the supplied +order, and one column per sampled time. `multi_time_times` supplies the matching +time axis. The rows contain $C_{zz}$ and $C_{zx}$ for Pauli operators; divide by +four for spin-$1/2$ correlations. In particular, $C_{zz}(0)=1$. -In dynamical typicality studies, one often averages correlations over an -ensemble of initial states. Under certain thermalisation guarantees, one can -show that the typical relaxation behavior of _any_ state can be represented by -an ensemble average of the expectation over randomly initialised states. For -sufficiently rich ensembles, this can approximate high-temperature traces and -reveal robust transport trends. +## 3. Average over initial states -YAQS supports this directly by passing `list[State]` into `Simulator.run`. Each -member evolves independently, which, when parallelized by the unitary backend, -offers computational advantage to calculate these variables. +Pass several states to average the same correlations with equal weights. Keep +the original state as the first member and add three independently initialized +random MPS. Each state evolves separately, and the list length determines the +ensemble size; `num_traj` does not set it. -```{code-cell} ipython3 +```{code-cell} python num_states = 4 -ensemble_states = [State(L, initial="haar-random", pad=2) for _ in range(num_states)] - +ensemble_states = [psi0, *[State(L, initial="haar-random", pad=2) for _ in range(num_states - 1)]] ensemble_params = AnalogSimParams( observables=[], elapsed_time=5.1, dt=0.15, max_bond_dim=64, svd_threshold=1e-10, - sample_timesteps=True, - multi_time_observables=[ - (Observable("z", mid), Observable("z", mid)), # C_zz(t) autocorrelation - (Observable("z", mid), Observable("x", mid)), # C_zx(t) - ], + multi_time_observables=[(sz_mid, sz_mid), (sz_mid, sx_mid)], ) - result_ens = sim.run(ensemble_states, H_open, ensemble_params) t_ens = result_ens.multi_time_times czz_ens = result_ens.multi_time_results[0] czx_ens = result_ens.multi_time_results[1] ``` -```{code-cell} ipython3 -fig, ax = plt.subplots(1, 1, figsize=(5.8, 3.4)) -ax.plot(t_ens, np.real(czz_ens), "o-", label=r"ensemble $C_{zz}(t)$") -ax.plot(t_ens, np.real(czx_ens), "s--", label=r"ensemble $C_{zx}(t)$") -ax.set_xlabel("t") -ax.set_ylabel(r"$\overline{C}_{ab}(t)$") -ax.set_title(f"Typicality-style ensemble average of $C_{{zz}}(t)$ and $C_{{zx}}(t)$ (N={num_states})") -ax.legend() -ax.grid(alpha=0.3) +`multi_time_results` now contains the ensemble mean, with the same pair and time +axes. Ordinary `observables`, if supplied, also produce ensemble means in +`expectation_values` and individual member data in `trajectories`. Per-member +two-time correlations are not exposed in `Result`. + +```{code-cell} python +:tags: [hide-input] +fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9), layout="constrained") +for ax, single, average, label in zip( + axes, [czz_single, czx_single], [czz_ens, czx_ens], ["zz", "zx"], strict=True, +): + ax.plot(t_single, single.real, color="0.55", linestyle="--", linewidth=1.3, label="One state") + ax.plot(t_ens, average.real, color="#0072B2", linewidth=1.8, label=f"{num_states}-state mean") + ax.axhline(0, color="0.75", linewidth=0.6) + ax.set(xlabel=r"Time $t$", ylabel=rf"$\mathrm{{Re}}\,C_{{{label}}}(t)$", xlim=(0, 5.1)) + ax.legend(fontsize=8) +axes[0].set_title("(a) Autocorrelation", loc="left", fontsize=11) +axes[1].set_title("(b) Cross correlation", loc="left", fontsize=11) plt.show() ``` -In this illustrative run, the ensemble-averaged $C_{zz}(t)$ appears to decay -toward zero while $C_{zx}(t)$ stays comparatively close to zero over the sampled -window; other runs may show different behavior. +The plots compare the real parts; the result retains both real and imaginary +components. Averaging changes the state-dependent fluctuations, but four states +do not establish a converged thermal average. This example demonstrates the +ensemble workflow. Dynamical quantum typicality uses suitable random-state +sampling to estimate traces; finite-temperature calculations also need thermal +weighting or filtering. The small random-MPS ensemble here supplies neither a +convergence study nor finite-temperature preparation. -## 4. Spin transport example: periodic spin-current autocorrelation +## 4. Compare local spin-current correlations -For periodic XXZ chains, define local bond current +A periodic XXZ chain lets us study how spin currents change with the interaction +strength. For each directed bond $(r,r+1)$, with site indices wrapped modulo +$L$, define ```{math} -j_r = J_{xx} \bigl(S_r^x S_{r+1}^y - S_r^y S_{r+1}^x\bigr) +j_r=J_{xx}\left(S_r^x S_{r+1}^y-S_r^y S_{r+1}^x\right). ``` -and total current $J = \sum_r j_r$. The normalized autocorrelator +Measure each bond's autocorrelation and average over bonds and initial states: ```{math} -C_{JJ}(t) = \frac{1}{L}\,\langle J(t)\,J(0)\rangle +C_{\mathrm{bond}}(t)=\frac{1}{L}\sum_r\langle j_r(t)j_r(0)\rangle_{\mathrm{ensemble}}. ``` -can be assembled from all bond-pair two-time correlators. Such current -autocorrelations are central to linear-response spin transport; dynamical -typicality makes it practical to estimate high-temperature ensemble quantities -from a few random pure-state trajectories -([Steiningeweg _et al._, Phys. Rev. Lett. **112**, 120601 (2014)](https://doi.org/10.1103/PhysRevLett.112.120601)). -For finite-temperature Drude weights, diffusion, and integrable XXZ -phenomenology—including the role of conservation laws—see the review -([Bertini _et al._, Rev. Mod. Phys. **93**, 025003 (2021)](https://doi.org/10.1103/RevModPhys.93.025003)). - -```{code-cell} ipython3 -def spin_current_bond_matrix(j_coupling: float) -> np.ndarray: - x = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex128) - y = np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=np.complex128) - return 0.25 * j_coupling * (np.kron(x, y) - np.kron(y, x)) - +The two-site matrix below follows the listed site order, including the periodic +bond `(L - 1, 0)`. -def periodic_bonds(length: int) -> list[tuple[int, int]]: - return [(i, (i + 1) % length) for i in range(length)] +```{code-cell} python +def spin_current_bond_matrix(j_coupling): + x = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) + y = np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex) + return 0.25 * j_coupling * (np.kron(x, y) - np.kron(y, x)) -def current_observables(length: int, j_coupling: float) -> list[Observable]: - j_mat = spin_current_bond_matrix(j_coupling) - return [Observable(j_mat, sites=[i, j]) for i, j in periodic_bonds(length)] -``` - -```{code-cell} ipython3 Ltr = 6 -Jxx = 1.0 -# Keep the docs build light: two anisotropies and bond autocorrelations only -# (full J_i–J_j cross terms are L^2 correlators and time out on Read the Docs). deltas = [0.1, 1.5] -t_final = 3.0 -dt = 0.2 -n_transport_states = 2 - -states_transport = [State(Ltr, initial="haar-random", pad=2) for _ in range(n_transport_states)] -bond_obs = current_observables(Ltr, Jxx) +states_transport = [State(Ltr, initial="haar-random", pad=2) for _ in range(2)] +j_mat = spin_current_bond_matrix(Jxx) +bond_obs = [Observable(j_mat, sites=[r, (r + 1) % Ltr]) for r in range(Ltr)] pairs_jj = [(obs, obs) for obs in bond_obs] - -transport_curves: dict[float, np.ndarray] = {} -t_transport = None +transport_curves = {} +transport_results = {} for d in deltas: h_periodic = Hamiltonian.pauli( length=Ltr, @@ -261,59 +235,63 @@ for d in deltas: one_body=[], bc="periodic", ) - sp = AnalogSimParams( + transport_params = AnalogSimParams( observables=[], - elapsed_time=t_final, - dt=dt, + elapsed_time=3.0, + dt=0.1, max_bond_dim=32, svd_threshold=1e-10, - sample_timesteps=True, multi_time_observables=pairs_jj, ) - result_transport = sim.run(states_transport, h_periodic, sp) + result_transport = sim.run(states_transport, h_periodic, transport_params) t_transport = result_transport.multi_time_times - c_jj = np.real(np.sum(result_transport.multi_time_results, axis=0) / Ltr) - transport_curves[d] = c_jj + transport_results[d] = result_transport + transport_curves[d] = result_transport.multi_time_results.mean(axis=0) ``` -```{code-cell} ipython3 -fig, ax = plt.subplots(1, 1, figsize=(6.0, 3.5)) -for d in deltas: - ax.plot(t_transport, transport_curves[d], marker="o", ms=3, label=rf"$\Delta={d}$") -ax.set_xlabel("t") -ax.set_ylabel(r"$C_{JJ}(t)$") -ax.set_title("Periodic XXZ spin-current autocorrelation (small illustrative setup)") -ax.legend() -ax.grid(alpha=0.3) +Each `multi_time_results` array has shape `(6, 31)`, with one row for each bond. +The final mean over rows gives $C_{\mathrm{bond}}$. Reusing the same initial +states for both interaction strengths keeps the ensemble fixed. + +```{code-cell} python +:tags: [hide-input] +fig, ax = plt.subplots(figsize=(5.4, 2.9), layout="constrained") +for d, color in zip(deltas, ["#0072B2", "#D55E00"], strict=True): + ax.plot(t_transport, transport_curves[d].real, color=color, linewidth=1.8, label=rf"$\Delta={d}$") +ax.axhline(0, color="0.75", linewidth=0.6) +ax.set(xlabel=r"Time $t$", ylabel=r"$\mathrm{Re}\,C_{\mathrm{bond}}(t)$", xlim=(0, 3)) +ax.set_title("Local spin-current autocorrelation", loc="left", fontsize=11) +ax.legend(fontsize=9) plt.show() ``` -This finite-size, short-time run already shows different relaxation trends for -different anisotropies. In the thermodynamic limit and Kubo picture, the -long-time behavior of $C_{JJ}(t)$ is tied to the spin Drude weight and to -ballistic versus diffusive transport in the XXZ chain; -[Bertini _et al._, Rev. Mod. Phys. **93**, 025003 (2021)](https://doi.org/10.1103/RevModPhys.93.025003) -summarizes the established finite-temperature picture (including subtleties at -$\Delta=1$ and in finite systems). The illustrative curves here use small $L$ -and a handful of Haar-random states; larger-scale or higher-accuracy studies -follow typicality, as in -[Steiningeweg _et al._, Phys. Rev. Lett. **112**, 120601 (2014)](https://doi.org/10.1103/PhysRevLett.112.120601). - -:::{tip} Practical notes: scaling runs and MPS entanglement - -- Scale gradually: `L`, ensemble size, `dt`, `elapsed_time`, and `max_bond_dim`. -- Enable ensemble parallelization (`Simulator(parallel=True)`) when you have - many initial states. -- **MPS entanglement:** under unitary evolution, entanglement entropy and - required bond dimension typically **grow** with time (until truncation or - saturation). For longer times or larger $L$, increase `max_bond_dim`, tighten - `svd_threshold` only with care, or shorten the window so the MPS remains an - accurate ansatz for your observable. - -::: - -## Related topics - -- {doc}`analog_simulation` — noisy and unitary TJM evolution -- {doc}`state_initialization` — Haar-random and ensemble `list[State]` inputs -- {doc}`simulator_initialization` — `Simulator(parallel=True)` for ensemble runs +The curves show how the bond-current correlation depends on the interaction +strength over this short window. They are not the full total-current +correlation. For $J=\sum_r j_r$, the latter contains all cross-bond terms, +$C_{JJ}(t)=L^{-1}\sum_{r,s}\langle j_r(t)j_s(0)\rangle$. Computing it requires +$L^2$ operator pairs instead of the $L$ pairs used here. + +These small-chain curves do not determine a diffusion constant or a Drude +weight. For the connection between typicality and current correlations, see +[Steinigeweg et al., Phys. Rev. Lett. **112**, 120601 (2014)](https://doi.org/10.1103/PhysRevLett.112.120601). +The broader transport setting is covered in +[Bertini et al., Rev. Mod. Phys. **93**, 025003 (2021)](https://doi.org/10.1103/RevModPhys.93.025003). + +## Scale the calculation + +Parallel execution is enabled by default for ensembles with several members. The +documentation suppresses progress with `show_progress=False`; omit that setting +to see progress. Increase the number of initial states to check sampling +convergence, and check timestep and bond-dimension convergence separately. The +random state's initial `pad` and the evolution's `max_bond_dim` serve different +purposes. Longer evolution can require larger bonds as entanglement grows. + +The list-of-state path requires MPS inputs and a static Hamiltonian. It returns +ensemble observables and correlations, rather than a final ensemble state. + +## Related guides + +- {doc}`analog_simulation` — single-state analog evolution and numerical + settings. +- {doc}`state_initialization` — random MPS, custom states, and list inputs. +- {doc}`simulator_initialization` — parallel workers and script execution. From 9db9898304f2746ef57b63f2a3682332c856c8b2 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:55:47 +0200 Subject: [PATCH 19/30] updated docs --- docs/examples/analog_simulation.md | 2 +- docs/examples/circuit_observables.md | 144 ++++- docs/examples/circuit_shots.md | 2 +- docs/examples/custom_gates.md | 343 +---------- docs/examples/digital_analog_simulation.md | 4 +- docs/examples/equivalence_checking.md | 2 +- docs/examples/hamiltonians.md | 640 ++++++++------------- docs/examples/realistic_noise_models.md | 607 +++++++++---------- docs/examples/representation_comparison.md | 359 ++++++++---- docs/examples/scheduled_jumps.md | 176 +----- docs/examples/simulation_parameters.md | 527 +++++++---------- docs/examples/simulator_initialization.md | 470 +++++++-------- docs/examples/state_initialization.md | 319 +++++----- docs/index.md | 6 +- 14 files changed, 1506 insertions(+), 2095 deletions(-) diff --git a/docs/examples/analog_simulation.md b/docs/examples/analog_simulation.md index f29d2b47f..bdbdadb8f 100644 --- a/docs/examples/analog_simulation.md +++ b/docs/examples/analog_simulation.md @@ -320,5 +320,5 @@ choices. Hamiltonians - {doc}`representation_comparison` — MPS, statevector, and density matrix backends -- {doc}`scheduled_jumps` — deterministic jumps at specified times +- {ref}`noise-scheduled-jumps` — deterministic jumps at specified times - {doc}`ensemble_evolution` — unitary ensemble correlations diff --git a/docs/examples/circuit_observables.md b/docs/examples/circuit_observables.md index d2be1517e..fd65ebbde 100644 --- a/docs/examples/circuit_observables.md +++ b/docs/examples/circuit_observables.md @@ -294,6 +294,8 @@ refinement, rebuild the circuit and rescale the strengths using the new step and gate exposures. Finite-step noise splitting can also affect the spatial profile; agreement of total excitation alone does not validate that profile. +(circuit-qasm-inputs)= + ## 7. OpenQASM inputs Pass an OpenQASM 2 source string (or file path) directly to @@ -334,7 +336,8 @@ see {doc}`equivalence_checking`. `DigitalSimParams.gate_mode` selects how two-qubit gates are applied to the MPS. The default `"mpo"` uses extended gate MPOs for long-range pairs; `"tdvp"` uses a local TDVP window when an analytic generator is available. See -{doc}`simulation_parameters` and {doc}`custom_gates` for the full matrix. +{doc}`simulation_parameters` for the available modes and +{ref}`circuit-custom-gates` for matrix-backed gates. Below, a long-range `cx` on qubits 0 and 2 is simulated noiselessly with both modes: @@ -359,13 +362,150 @@ for mode in ("mpo", "tdvp"): print({mode: round(value, 4) for mode, value in z0_by_mode.items()}) ``` +(circuit-custom-gates)= + +## 9. Supply custom gates + +Add a custom unitary to a Qiskit circuit with `UnitaryGate`. YAQS translates the +matrix automatically, so no gate registration is needed. This two-qubit example +applies a phase only to the $|11\rangle$ component: + +```{code-cell} python +from qiskit.circuit.library import UnitaryGate + +custom_unitary = np.diag([1, 1, 1, np.exp(0.4j)]) +custom_circuit = QuantumCircuit(2) +custom_circuit.h([0, 1]) +custom_circuit.append(UnitaryGate(custom_unitary), [0, 1]) + +custom_params = DigitalSimParams(observables=[Observable("x", 0)]) +custom_sim = Simulator(show_progress=False) +custom_result = custom_sim.run(State(2, initial="zeros"), custom_circuit, custom_params) +print(custom_result.expectation_values[0]) +``` + +The final expectation is $\langle X_0\rangle=(1+\cos 0.4)/2$, about 0.9605. The +matrix uses Qiskit's qubit ordering; YAQS converts it to its internal gate +layout. The same input works with `shots`, noise, and sampling checkpoints under +the circuit rules described above. + +Custom gate bodies in {ref}`circuit-qasm-inputs` follow the same translation +path. Unknown unitary operations use a matrix fallback on up to eight qubits; +decompose larger operations first. A matrix-backed gate has no analytic +generator: TDVP gate modes use direct local updates for adjacent pairs and the +MPO path for separated sites or larger gates. Keep `gate_mode="mpo"` unless you +need another method. + +:::{dropdown} Supported instructions and gate translation + +Bind symbolic Qiskit parameters before simulation. YAQS translates known gate +names through its gate library; other operations must provide a unitary matrix +through Qiskit's `to_matrix()` or `Operator`. This also supports gates defined +by a reusable Qiskit circuit or an OpenQASM gate body. Use a distinct name for a +custom operation, since a name matching a built-in alias selects that built-in +implementation. + +Terminal measurements are removed before simulation; request `shots` for +readout. Measurements followed by further operations on the measured qubits are +unsupported. `reset`, `delay`, `store`, classical conditions, and control-flow +instructions are also unsupported. Ordinary barriers do not change the state; +barriers labelled `SAMPLE_OBSERVABLES` mark sampling points. + +Digital gates require qubit target sites. Idle sites can have other local +dimensions, but `gate_mode="swaps"` cannot route through a non-qubit site. + +Built-in `ccx`, `ccz`, and `cswap` gates translate without decomposition. +Simulation applies gates on three or more qubits through an MPO, except +supported product generators such as `ccx` and `ccz` in TDVP modes. The +`"swaps"` mode also uses the MPO path for these larger gates. + +`EquivalenceChecker` accepts the same unitary and OpenQASM inputs. Gates on more +than two qubits require its `"matrix"` backend; decompose them before using the +`"mpo"` backend. See {doc}`equivalence_checking` for backend choice and +measurement restrictions. Translation details and the supported alias list are +in {mod}`~mqt.yaqs.digital.utils.dag_utils`. + +::: + +:::{dropdown} Low-level gate objects + +Application code should supply Qiskit circuits. For code that works directly +with YAQS gate kernels, `GateLibrary.custom` constructs a matrix-backed +{class}`~mqt.yaqs.core.libraries.gate_library.BaseGate`: + +```python +from mqt.yaqs.core.libraries.gate_library import GateLibrary + +gate = GateLibrary.custom(np.eye(4, dtype=complex)) +gate.name = "my_gate" +gate.set_sites(0, 1) +``` + +This constructor checks that the matrix is square with dimension $2^n$. The +caller must supply a finite unitary. The resulting fields are: + +| Field | Meaning | +| ------------- | -------------------------------------------------------------- | +| `matrix` | Gate matrix in YAQS gate order. | +| `interaction` | Number of target qubits, inferred from the matrix size. | +| `sites` | Target sites in their declared order. | +| `tensor` | Gate tensor; `set_sites` reshapes gates on two or more qubits. | +| `generator` | Optional local factors for a product-form TDVP generator. | +| `name` | Gate identifier. | + +In a manually supplied gate matrix, the first tensor factor acts on the first +declared site. Qiskit translation handles its different matrix convention +automatically. Creating this object does not register a new Qiskit gate or make +it a valid operator argument to `Simulator.run`. + +Built-in gates subclass `BaseGate` and prepare tensors and optional generators +in `set_sites`. See {class}`~mqt.yaqs.core.libraries.gate_library.CX` and +{class}`~mqt.yaqs.core.libraries.gate_library.CCX` for examples. + +::: + +:::{dropdown} Product generators for digital TDVP + +A TDVP-capable gate has one $2\times2$ generator factor per target site. The +factors define a product $G$ whose exponential at evolution time one must +reproduce the gate, $U=\exp(-iG)$. For example, a ZZ phase rotation has +$G=(\theta Z/2)\otimes Z$: + +```python +from scipy.linalg import expm + +from mqt.yaqs.core.libraries.gate_library import GateLibrary + +theta = 0.3 +pauli_z = np.diag([1.0, -1.0]) +generator_factors = [theta * pauli_z / 2, pauli_z] +phase_unitary = expm(-1j * np.kron(*generator_factors)) +phase_gate = GateLibrary.custom(phase_unitary) +phase_gate.set_sites(0, 2) +phase_gate.generator = generator_factors +``` + +The factors follow the declared `sites` order. YAQS places identities between +separated factors when constructing the generator MPO. The caller must check +that the full generator is Hermitian and reproduces the unitary; YAQS does not +verify that relation. This low-level assignment does not change how a Qiskit +`UnitaryGate` is translated. + +Digital generator evolution requires `tdvp_mode="2site"`. `tdvp_sweeps` divides +the total generator time of one into substeps. TDVP gate application remains +approximate and can miss required bond growth; see {doc}`simulation_parameters` +for accuracy limits. Single-qubit gates always use direct contraction. For +implementation details, see +{func}`~mqt.yaqs.digital.digital_tjm.construct_generator_mpo`. + +::: + ## Related topics - {doc}`digital_analog_simulation` — combine digital operations with analog evolution in one program - {doc}`circuit_shots` — computational-basis shot histograms with {class}`~mqt.yaqs.DigitalSimParams` -- {doc}`custom_gates` — custom unitaries and gate translation - {doc}`realistic_noise_models` — log-normal and other distributed noise strengths - {doc}`equivalence_checking` — verify that two circuits implement the same diff --git a/docs/examples/circuit_shots.md b/docs/examples/circuit_shots.md index e471f9a89..02929db4a 100644 --- a/docs/examples/circuit_shots.md +++ b/docs/examples/circuit_shots.md @@ -233,5 +233,5 @@ checkpoints, and gate-application modes. - {doc}`simulation_parameters` — sampling budgets and accuracy presets - {doc}`realistic_noise_models` — other channels, custom operators, and disorder -- {doc}`custom_gates` — custom unitaries and gate translation +- {ref}`circuit-custom-gates` — custom unitaries and gate translation - {doc}`equivalence_checking` — compare circuit behavior diff --git a/docs/examples/custom_gates.md b/docs/examples/custom_gates.md index 0efc73ba4..df4e51f77 100644 --- a/docs/examples/custom_gates.md +++ b/docs/examples/custom_gates.md @@ -1,341 +1,12 @@ -# Custom Gates in YAQS - -```{note} -This is a **reference guide** with static code blocks; it is not executed during -the documentation build. Runnable circuit examples are in -{doc}`circuit_observables` and {doc}`equivalence_checking`. -``` - -YAQS represents every digital gate as a -{class}`~mqt.yaqs.core.libraries.gate_library.BaseGate` instance from -{class}`~mqt.yaqs.core.libraries.gate_library.GateLibrary`. Circuits enter YAQS -as Qiskit {class}`qiskit.circuit.QuantumCircuit` objects; the library converts -each DAG operation into an internal gate, then applies that gate during -**circuit simulation** or **equivalence checking**. - -This page explains that pipeline, how built-in and custom gates are translated, -and how to supply an analytic **generator** when you want long-range and -multi-qubit gates to use the TDVP window path. - -## How YAQS handles gates end-to-end - -```mermaid -flowchart TD - qc[QuantumCircuit] --> dag[DAGCircuit] - dag --> convert[convert_dag_to_tensor_algorithm] - convert --> hardcoded[GateLibrary alias] - convert --> fallback[GateLibrary.custom matrix] - hardcoded --> bg[BaseGate] - fallback --> bg - bg --> sim[Simulator / digital_tjm] - bg --> equiv[EquivalenceChecker] - sim --> mps[MPS site updates] - equiv --> mpo[MPO or dense tensor backend] -``` - -### Internal gate objects - -Each {class}`~mqt.yaqs.core.libraries.gate_library.BaseGate` carries: - -| Field | Role | -| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `matrix` | Dense unitary as a `2^n × 2^n` complex matrix (`n` = number of qubits the gate acts on). | -| `tensor` | Tensor layout used in MPS/MPO contractions (shape `(2,) * (2 * interaction)` after `set_sites` for gates on two or more qubits). | -| `interaction` | Number of qubits (`1`, `2`, …); inferred from `matrix` size (`dim = 2**interaction`). | -| `sites` | MPS site indices the gate acts on, set via `set_sites(...)`. | -| `generator` | Optional. For built-ins that support TDVP: a list with one `2×2` local operator per qubit used to build a generator MPO. Not set by plain `GateLibrary.custom(...)`. | -| `name` | Qiskit operation name or `"custom"`. | - -Built-in gates (`cx`, `h`, `rxx`, …) are classes on -{class}`~mqt.yaqs.core.libraries.gate_library.GateLibrary`. -`GateLibrary.custom(matrix)` returns a generic -{class}`~mqt.yaqs.core.libraries.gate_library.BaseGate` backed only by the -unitary matrix. - -### Translation from Qiskit - -{func}`~mqt.yaqs.digital.utils.dag_utils.convert_dag_to_tensor_algorithm` walks -a {class}`qiskit.dagcircuit.DAGCircuit` and produces a list of `BaseGate` -objects. - -For each operation node: - -1. **Unsupported instructions** raise `ValueError`: `reset`, `delay`, `store`, - mid-circuit `measure`, classically controlled ops, and control-flow - instructions (`if_else`, `for_loop`, …). -2. **`barrier`** nodes are skipped during conversion (ignored for unitary - evolution). -3. **Known Qiskit names** (for example `h`, `cx`, `u3`, `u1`, `swap`, `rxx`) map - to hardcoded {class}`~mqt.yaqs.core.libraries.gate_library.GateLibrary` - classes via `getattr(GateLibrary, name)`. -4. **Any other unitary** falls back to the **matrix path**: Qiskit's - `to_matrix()` / {class}`qiskit.quantum_info.Operator` data is wrapped as - `GateLibrary.custom(matrix)` with the Qiskit `op.name` preserved. - -```{note} -**Three-qubit and larger gates** translate natively: `ccx` (Toffoli), `ccz`, and -`cswap` are hardcoded {class}`~mqt.yaqs.core.libraries.gate_library.GateLibrary` -classes, and any other multi-qubit unitary on up to eight qubits is accepted on -the matrix fallback path. -``` - -Symbolic parameters must be bound before translation; unbound -{class}`qiskit.circuit.Parameter` objects raise `ValueError`. - -### Application during circuit simulation - -{class}`~mqt.yaqs.Simulator` runs digital circuits through -{mod}`~mqt.yaqs.digital.digital_tjm`: - -- **Single-qubit gates** — contract the gate tensor onto the corresponding MPS - site. -- **Two-qubit gates** — routed by `DigitalSimParams.gate_mode` (see - {doc}`simulation_parameters`): - - **`mpo`** (default) — TEBD/SVD on nearest-neighbor pairs; long-range gates - via extended gate MPO. - - **`swaps`** — TEBD with SWAP routing for long-range pairs. - - **`tdvp`** — TEBD on nearest-neighbor pairs; long-range pairs use **2TDVP** - only when the gate has a `generator`; otherwise the MPO path is used. - - **`full-tdvp`** — 2TDVP on every two-qubit gate that has a `generator`; - generator-less gates fall back to TEBD (NN) or MPO (long-range). -- **Gates on three or more qubits** — in the TDVP modes, gates with a - `generator` (`ccx`, `ccz`) use the generator MPO and TDVP window; all other - cases, including `gate_mode="swaps"`, use the extended gate MPO. - -**Measurements** are handled differently from unitary conversion: - -- During **simulation**, terminal `measure` nodes are removed from the live DAG - so evolution can proceed; sampling uses the remaining circuit structure. -- During **equivalence checking**, final measurements are stripped from both - circuits before comparison; mid-circuit measurements raise `ValueError`. - -Plain `barrier` instructions are dropped in simulation except barriers labelled -`SAMPLE_OBSERVABLES` (used for digital layer sampling). - -### Equivalence checking - -{class}`~mqt.yaqs.EquivalenceChecker` compares two circuits by forming -$W = U_1 U_2^\dagger$ and testing whether $W$ is identity-like up to global -phase. Custom and QASM-defined gates use the same translation path as -simulation; unknown unitaries work via matrix fallback. Gates on more than two -qubits require the matrix backend; use `representation="matrix"` explicitly when -`auto` would select the MPO backend. See {doc}`equivalence_checking` for backend -choice (`representation="mpo"` recommended for one- and two-qubit circuits at -scale). - --- - -## Custom gates from Qiskit (most common) - -You do **not** need to register custom gates manually when they come from -Qiskit. - -### `UnitaryGate` - -```python -import numpy as np -from qiskit import QuantumCircuit -from qiskit.circuit.library import UnitaryGate - -u = np.array([[0, 1], [1, 0]], dtype=complex) -qc = QuantumCircuit(1) -qc.append(UnitaryGate(u), [0]) -``` - -YAQS translates this to a matrix-backed `BaseGate` named `"unitary"` with **no** -`generator` attribute. - -### OpenQASM 2 custom gates - -OpenQASM 2 lets you declare reusable gate bodies (fixed or parameterized) and -call them like built-in instructions. Pass a file path or raw OpenQASM string -directly to {meth}`~mqt.yaqs.EquivalenceChecker.check` or -{meth}`~mqt.yaqs.Simulator.run`, or load with `qiskit.qasm2.loads` / `load` -first. Qiskit produces a {class}`qiskit.circuit.QuantumCircuit` whose operations -retain the **user-defined gate names**. - -YAQS does not maintain a separate registry of QASM gate definitions. Each DAG -node is translated by name: if the name matches a -{class}`~mqt.yaqs.core.libraries.gate_library.GateLibrary` entry, the hardcoded -class is used; otherwise YAQS builds a matrix-backed gate from Qiskit's unitary -representation (matrix fallback). You do not need to inline or transpile custom -gates to a fixed basis set before simulation or equivalence checking. - -```python -from mqt.yaqs import EquivalenceChecker, Simulator, State, DigitalSimParams - -qasm = """ -OPENQASM 2.0; -include "qelib1.inc"; - -gate entangle a,b { - h a; - cx a,b; -} - -qreg q[2]; -entangle q[0], q[1]; -""" - -checker = EquivalenceChecker(representation="mpo") -checker.check(qasm, qasm) - -state = State(2, initial="zeros") -Simulator(show_progress=False).run(state, qasm, DigitalSimParams(shots=128, max_bond_dim=4)) -``` - -The same rules apply to **legacy or backend-specific Qiskit gate names** that -are not in the hardcoded alias list: if Qiskit can supply a unitary matrix, -translation succeeds via matrix fallback. Names that already have -{class}`~mqt.yaqs.core.libraries.gate_library.GateLibrary` aliases (including -common single-qubit parameterizations) continue to use the built-in -implementations. - -### TDVP behaviour for matrix-backed custom gates - -Matrix-backed custom gates have **no analytic generator**. In `gate_mode="tdvp"` -or `"full-tdvp"`: - -| Gate width | Routing | -| -------------------------------- | --------------------------------------------- | -| Nearest-neighbor (\|i − j\| = 1) | TEBD/SVD | -| Long-range (\|i − j\| > 1) | Extended gate MPO (same as `gate_mode="mpo"`) | -| Three or more qubits | Extended gate MPO (same as `gate_mode="mpo"`) | - -This is intentional: 2TDVP requires a split local generator; a bare unitary -matrix does not provide one. Use `gate_mode="mpo"` if you want consistent -long-range handling for all custom unitaries without defining generators. - ---- - -## Manually defining custom gates - -Use this path for library code, tests, or workflows that construct gates -directly in YAQS (without Qiskit). - -### Matrix-only custom gate - -```python -import numpy as np -from mqt.yaqs.core.libraries.gate_library import GateLibrary - -unitary = np.eye(4, dtype=complex) # example 2-qubit unitary -gate = GateLibrary.custom(unitary) -gate.name = "my_gate" -gate.set_sites(0, 1) -# gate.matrix, gate.tensor, gate.sites are ready for TEBD/MPO application -``` - -`GateLibrary.custom` validates that the matrix is square with dimension $2^n$. - -For **equivalence checking**, comparing two circuits that use the same custom -unitary (or a custom gate versus an equivalent decomposition) works the same as -for built-in gates. - -### Subclassing `BaseGate` - -Built-in gates subclass {class}`~mqt.yaqs.core.libraries.gate_library.BaseGate` -and often override `set_sites` to set `tensor`, optional `mpo_tensors`, and—for -TDVP-capable gates—`generator`. See -{class}`~mqt.yaqs.core.libraries.gate_library.CX` and -{class}`~mqt.yaqs.core.libraries.gate_library.CCX` in -{mod}`~mqt.yaqs.core.libraries.gate_library` for reference implementations. - ---- - -## Generators and the TDVP path - -Some {class}`~mqt.yaqs.core.libraries.gate_library.GateLibrary` gates (`cx`, -`cz`, `cp`, `rxx`, `ryy`, `rzz`, `ccx`, `ccz`, …) define a **`generator`**: a -list with one `2×2` complex matrix per qubit, ordered as the gate's declared -`sites`. - -{func}`~mqt.yaqs.digital.digital_tjm.construct_generator_mpo` places each local -operator on its declared site and identity `2×2` blocks elsewhere, producing an -MPO that represents the product generator on the full chain. -{func}`~mqt.yaqs.digital.digital_tjm.apply_two_qubit_gate_tdvp` then runs -**two-site TDVP** (`tdvp_mode="2site"`) on a window around the gate for a total -evolution time of **1** (split across `tdvp_sweeps` substeps on -{class}`~mqt.yaqs.core.data_structures.simulation_parameters.DigitalSimParams`). - -The controlled-NOT gate illustrates the pattern (see source for exact matrices): - -```python -from mqt.yaqs.core.libraries.gate_library import GateLibrary - -gate = GateLibrary.cx() -gate.set_sites(0, 1) -# gate.generator is a list of two 2x2 arrays, set inside set_sites -``` - -Routing checks `getattr(gate, "generator", None) is not None` in -{func}`~mqt.yaqs.digital.digital_tjm.apply_two_qubit_gate`. Plain -`GateLibrary.custom(...)` does **not** set `generator`; TDVP long-range -application therefore does not activate unless you add one yourself. - -### Attaching a generator to a custom gate - -Advanced use: if you know a product-form generator decomposition compatible with -YAQS digital TDVP, you can assign it after `set_sites` (one `2×2` factor per -site, in the declared site order): - -```python -import numpy as np -from mqt.yaqs.core.libraries.gate_library import GateLibrary - -unitary = ... # 4x4 unitary on qubits (i, j), i < j -G_i = ... # 2x2 local operator on site i -G_j = ... # 2x2 local operator on site j - -gate = GateLibrary.custom(unitary) -gate.set_sites(i, j) -gate.generator = [G_i, G_j] -``` - -```{warning} -YAQS does not verify that `exp(-i (G_i ⊗ G_j))` at evolution time `1` reproduces -`unitary`. Deriving consistent local generators is the caller's responsibility; -use built-in gates as templates. Each factor is paired with the site declared at -the same position in `sites`; -{func}`~mqt.yaqs.digital.digital_tjm.construct_generator_mpo` places the factors -by site. -``` - -Generators apply to digital gates on **two or more qubits**. Single-qubit custom -gates always use direct MPS contraction; there is no single-qubit TDVP gate path -in circuit simulation. - +orphan: true --- -## Quick reference - -| Source | `generator` | TDVP long-range (`gate_mode="tdvp"`) | Equivalence check | -| ------------------------------------------ | ---------------------- | ------------------------------------ | --------------------------- | -| Built-in `cx`, `rxx`, … | Yes (in `set_sites`) | 2TDVP window | Supported | -| Qiskit `UnitaryGate` / QASM custom | No | MPO fallback | Supported (matrix fallback) | -| `GateLibrary.custom(matrix)` | No (unless you set it) | MPO fallback | Supported | -| `GateLibrary.custom` + manual `.generator` | Yes (if you set it) | 2TDVP window | Supported | -| Built-in `ccx`, `ccz` | Yes (in `set_sites`) | 2TDVP window | Matrix backend only | -| `cswap`, 3+ qubit unitaries | No | MPO fallback | Matrix backend only | - -### Rejected Qiskit instructions (translation) - -| Instruction | Conversion | Simulation | Equivalence | -| ------------------------------ | -------------------------- | ------------------------------------- | --------------------- | -| `barrier` | Skipped | Removed (except `SAMPLE_OBSERVABLES`) | Skipped in MPO zones | -| Final `measure` | Rejected in raw conversion | Removed from DAG | Stripped before check | -| Mid-circuit `measure` | Rejected | Removed only if per-qubit terminal | `ValueError` | -| `reset`, `delay`, control-flow | Rejected | — | — | - ---- +# Custom Gates -## Related topics +See {ref}`circuit-custom-gates` for custom unitaries, supported circuit +instructions, and low-level gate construction. The circuit guide also covers +{ref}`circuit-qasm-inputs` and gate-application modes. -- {doc}`simulation_parameters` — `gate_mode`, `tdvp_sweeps`, `tdvp_mode` -- {doc}`equivalence_checking` — comparing original and transpiled circuits -- {doc}`circuit_observables` — running circuits with - {class}`~mqt.yaqs.Simulator` -- {mod}`~mqt.yaqs.digital.utils.dag_utils` — translation implementation and - `SUPPORTED_QISKIT_GATE_NAMES` -- {mod}`~mqt.yaqs.core.libraries.gate_library` — built-in gate definitions and - generator examples +For comparing circuits that contain custom gates, see +{doc}`equivalence_checking`. diff --git a/docs/examples/digital_analog_simulation.md b/docs/examples/digital_analog_simulation.md index 3d9fb5f64..9c8c00f71 100644 --- a/docs/examples/digital_analog_simulation.md +++ b/docs/examples/digital_analog_simulation.md @@ -305,8 +305,8 @@ contains the histogram from the last segment that sampled shots; inspect `segment_results` for earlier histograms. Program execution does not support `multi_time_observables`. -For deterministic scheduled jumps, see {doc}`scheduled_jumps`. Jump times use -the analog run's local clock and must follow its `dt` grid with `order=1`. +For deterministic scheduled jumps, see {ref}`noise-scheduled-jumps`. Jump times +use the analog run's local clock and must follow its `dt` grid with `order=1`. Consecutive compatible analog segments share that clock; a digital gate starts a new analog run. Use a segment noise override to attach a schedule to one interval. For device-specific noise strengths and distributions, see diff --git a/docs/examples/equivalence_checking.md b/docs/examples/equivalence_checking.md index 38aaf6788..65c9e7efe 100644 --- a/docs/examples/equivalence_checking.md +++ b/docs/examples/equivalence_checking.md @@ -297,7 +297,7 @@ and check that numerical truncation does not determine the answer. The returned Terminal measurements are ignored for unitary checks; mid-circuit measurements are unsupported. Decompose gates on more than two qubits before using the MPO -backend. See {doc}`custom_gates` for supported gate translation. +backend. See {ref}`circuit-custom-gates` for supported gate translation. ### Noise models and returned data diff --git a/docs/examples/hamiltonians.md b/docs/examples/hamiltonians.md index ef36b5e80..5e65e3f61 100644 --- a/docs/examples/hamiltonians.md +++ b/docs/examples/hamiltonians.md @@ -2,512 +2,330 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true execution_timeout: 120 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Building Hamiltonians -Analog simulations take a -{class}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian` as the operator -argument to {meth}`~mqt.yaqs.Simulator.run`. Most models are built as -**matrix product operators (MPOs)** under the hood; the `Hamiltonian` wrapper -materialises once at construction and can be reused across parameter sweeps. - -**Backend selection** is driven only by -{class}`~mqt.yaqs.core.data_structures.state.State` representation — not by how -the Hamiltonian was constructed: - -| `State.representation` | Backend | Form materialized at `run` | -| ---------------------- | -------- | -------------------------- | -| `"mps"` (default) | TJM | MPO | -| `"vector"` | MCWF | sparse | -| `"density_matrix"` | Lindblad | sparse | +A Hamiltonian defines the energies and interactions in an analog simulation. +Build one with a named model, a sum of Pauli terms, or your own operator data, +then pass it to `Simulator.run`. Match its site count and local dimensions to +those of the initial `State`. -Hamiltonian inputs (`tensors`, `matrix`, `sparse_matrix`, or a preset such as -`Hamiltonian.ising(...)`) are **source data**, not backend choices. The same -`Hamiltonian` instance works with all three state representations; -`Simulator.run` converts and caches the required MPO or sparse form. - -```{warning} -Selecting ``State.representation="mps"`` (TJM) calls -{meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.ensure_mpo`. For a -``sparse_matrix`` source this densifies the operator before MPO factorization, -allocating the full Hilbert-space matrix and risking out-of-memory failures on -large systems. Prefer an MPO preset, ``Hamiltonian.from_mpo(...)``, or -``tensors=`` when targeting TJM. -``` +## Choose a built-in model -This page covers the factory methods in the library. For open-system evolution -after the Hamiltonian is defined, see {doc}`analog_simulation`. For choosing a -state representation, see {doc}`state_initialization` and -{doc}`representation_comparison`. +Most builders create a matrix product operator (MPO), which stores the operator +as a tensor network. Use the `Hamiltonian` methods directly when available; wrap +an MPO with `Hamiltonian.from_mpo` for the other models. -## `Hamiltonian` versus `MPO` +| Model | Constructor | Site layout | +| ------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------- | +| Transverse-field Ising | {meth}`~mqt.yaqs.Hamiltonian.ising` | Qubits. | +| Heisenberg or XY | {meth}`~mqt.yaqs.Hamiltonian.heisenberg` | Qubits. | +| On-site and nearest-neighbor Pauli terms | {meth}`~mqt.yaqs.Hamiltonian.pauli` | Qubits. | +| Indexed Pauli strings, including long-range terms | {meth}`~mqt.yaqs.MPO.from_pauli_sum` | Qubits; wrap the MPO. | +| 1D Fermi–Hubbard | {meth}`~mqt.yaqs.Hamiltonian.fermi_hubbard_1d` | Dimension-four sites, or a Jordan–Wigner qubit chain. | +| Bose–Hubbard | {meth}`~mqt.yaqs.MPO.bose_hubbard` | Truncated boson occupation; wrap the MPO. | +| Coupled transmons and resonators | {meth}`~mqt.yaqs.Hamiltonian.coupled_transmon` | Alternating transmon and resonator dimensions. | +| Trapped ion position grid | {meth}`~mqt.yaqs.MPO.trapped_ion` | One grid per ion, for one or two ions; wrap the MPO. | -| Layer | Role | -| --------------------------------------------------------------- | ------------------------------------------------- | -| {class}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian` | User-facing type passed to `Simulator.run` | -| {class}`~mqt.yaqs.core.data_structures.mpo.MPO` | Tensor-network operator; built by factories below | +YAQS evolves with $\exp(-itH)$, using $\hbar=1$. Hamiltonian coefficients and +times must use consistent units. For energies in SI units, divide the operator +by $\hbar$ before evolving with time in seconds. -Typical patterns: +## Build a spin chain -- **Preset classmethods** — `Hamiltonian.ising(...)`, `Hamiltonian.pauli(...)`, - etc. (no `representation=` argument). -- **Wrap an MPO** — `Hamiltonian.from_mpo(mpo)` after `MPO.bose_hubbard(...)` or - a custom build. -- **Manual data** — `Hamiltonian(tensors=...)`, `Hamiltonian(matrix=...)`, or - `Hamiltonian(sparse_matrix=...)`. Any of these can drive TJM, MCWF, or - Lindblad once paired with the matching `State.representation`. -- **Piecewise time dependence** — `Hamiltonian.piecewise([(H, duration), ...])` - switches static Hamiltonians on the analog `dt` grid. +For an open chain, the Ising shortcut constructs -For a static Hamiltonian, access the materialized MPO with `H.mpo` (after a TJM -run or `H.ensure_mpo()`) and the sparse form with `H.sparse_matrix` (after an -MCWF / Lindblad run or `H.ensure_sparse()`). Both can coexist on one instance. A -piecewise Hamiltonian is a sequence of static pieces, so it cannot be -materialized with `ensure_mpo()` or `ensure_sparse()`. +$$ +H_{\mathrm{Ising}}=-J\sum_{i=0}^{L-2}Z_iZ_{i+1}-g\sum_{i=0}^{L-1}X_i. +$$ -## Hamiltonian energy - -For an MPS-backed state, contract the state with a static Hamiltonian's -materialized MPO: - -```{code-cell} ipython3 +```{code-cell} python from mqt.yaqs import Hamiltonian, State -state = State(4, initial="zeros") -hamiltonian = Hamiltonian.ising(4, J=1.0, g=0.5) -hamiltonian.ensure_mpo() -energy = state.mps.expect_mpo(hamiltonian.mpo) -``` - -This computes the raw value $\langle\psi|H_{\mathrm{MPO}}|\psi\rangle$ for the -cached MPO. If the Hamiltonian came from a dense or sparse matrix, the result -therefore includes any approximation made when that source was factorized into -an MPO. The method does not normalize the state. It returns the raw complex -value and does not require the MPO to be Hermitian. A Hamiltonian energy should -be real up to numerical error. - -A piecewise Hamiltonian has no single MPO or energy. Select the applicable -static piece first: - -```python -selected_hamiltonian, _duration = piecewise_hamiltonian.pieces[piece_index] -selected_hamiltonian.ensure_mpo() -energy = state.mps.expect_mpo(selected_hamiltonian.mpo) +length = 4 +hamiltonian = Hamiltonian.ising(length, J=1.0, g=0.5) +state = State(length, initial="zeros") ``` -## Long-range correlations +The Heisenberg shortcut uses the sign convention -Build an individual Pauli product or string as a full-chain MPO. Sites omitted -from the string act as identities, so the listed sites do not need to be -adjacent: +$$ +H_{\mathrm{Heisenberg}}=-\sum_{i=0}^{L-2} +\left(J_xX_iX_{i+1}+J_yY_iY_{i+1}+J_zZ_iZ_{i+1}\right) +-h\sum_{i=0}^{L-1}Z_i. +$$ -```{code-cell} ipython3 -from mqt.yaqs import MPO, Observable, State +Here, $X$, $Y$, and $Z$ are Pauli matrices with eigenvalues $\pm1$, not spin +operators with eigenvalues $\pm1/2$. Setting `Jz=0` gives the XY model used in +{doc}`analog_simulation`: -state = State(8, initial="zeros") -correlation = MPO() -correlation.from_pauli_sum( - terms=[(1.0, "Z0 Z7")], - length=state.mps.length, - n_sweeps=0, -) -zz_raw = state.mps.expect_mpo(correlation) +```{code-cell} python +xy = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) ``` -For a connected correlation, normalize each raw expectation value by the squared -state norm. For example, - -```{code-cell} ipython3 -norm_squared = state.mps.norm() ** 2 -z0_raw = state.mps.expect(Observable("z", 0)) -z7_raw = state.mps.expect(Observable("z", 7)) -zz_connected = zz_raw / norm_squared - z0_raw * z7_raw / norm_squared**2 -``` - -For a normalized state, this is -$\langle Z_0Z_7\rangle-\langle Z_0\rangle\langle Z_7\rangle$. The one-site terms -continue to use the existing local-observable API, including fast local -contraction when the tracked center covers the site. - -If one local matrix per site is already available, with identity matrices -between separated factors, `MPO.from_local_ops` builds the corresponding -bond-one tensor product. This workflow does not factor an arbitrary joint, -non-product matrix acting on separated sites or construct an optimized all-pairs -correlation matrix. Those features need separate decomposition, cutoff, -site-order, and environment-reuse contracts. - -## Time-dependent Hamiltonians - -Switch between static Hamiltonians at times that land on the analog `dt` grid. -Each duration must be a positive multiple of `dt`. This path supports a single -MPS `State` with TDVP. - -### Analog quench +These builders and `Hamiltonian.pauli` use open boundaries by default. Set +`bc="periodic"` to include the bond from the last site to site 0. -Use {meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.piecewise` -when the experiment is analog-only: evolve under one Hamiltonian, then another. -One {class}`~mqt.yaqs.AnalogSimParams`, one {class}`~mqt.yaqs.Result`, one time -grid. Pass ``elapsed_time=H.duration`` so the total time matches the pieces. +## Specify your own Pauli terms -```python -from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State +`Hamiltonian.pauli` repeats each `two_body` term over neighboring sites and each +`one_body` term over all sites. Coefficients enter with the sign you supply. For +example, the following builds the same XY Hamiltonian as above: -L = 4 -H = Hamiltonian.piecewise([ - (Hamiltonian.ising(L, J=1.0, g=0.5), 1.0), - (Hamiltonian.ising(L, J=1.0, g=2.0), 1.0), -]) -params = AnalogSimParams( - observables=[Observable("z", 0)], - elapsed_time=H.duration, - dt=0.1, +```{code-cell} python +xy_from_terms = Hamiltonian.pauli( + length=length, + two_body=[(-0.5, "X", "X"), (-0.5, "Y", "Y")], ) -result = Simulator().run(State(L, initial="zeros"), H, params) ``` -### Switching Hamiltonians in a program +Add an on-site field with an entry such as `one_body=[(-0.2, "Z")]`. These +structured builders require finite real coefficients and qubit sites. -Use a {class}`~mqt.yaqs.SimulationProgram` when analog evolution is part of a -protocol — digital gates, mixed `dt`, or per-segment noise. Consecutive analog -segments with the same `dt` match a piecewise Hamiltonian only when their analog -settings and noise behavior are compatible (evolution mode, order, sampling, -truncation, and a shared noise model). Mixed `dt` or a digital gate in between -splits the analog runs. A piecewise Hamiltonian is also valid on one analog -segment. +For site-dependent fields, separated sites, or longer strings, build an MPO from +explicit `(coefficient, string)` pairs: -```python -from mqt.yaqs import ( - AnalogSimParams, - Hamiltonian, - Observable, - SimulationProgram, - Simulator, - State, -) +```{code-cell} python +from mqt.yaqs import MPO -L = 4 -program = SimulationProgram( - [ - ( - Hamiltonian.ising(L, J=1.0, g=0.5), - AnalogSimParams(elapsed_time=1.0, dt=0.1), - ), - ( - Hamiltonian.ising(L, J=1.0, g=2.0), - AnalogSimParams(elapsed_time=1.0, dt=0.1), - ), - ], - observables=[Observable("z", 0)], +mpo = MPO() +mpo.from_pauli_sum( + terms=[(0.4, "Z0 Z3"), (0.2, "Y1"), (0.1, "")], + length=length, ) -result = Simulator().run(State(L, initial="zeros"), program) +custom = Hamiltonian.from_mpo(mpo) ``` -Insert a {class}`~qiskit.circuit.QuantumCircuit` between analog segments when -the protocol needs a digital operation. See {doc}`digital_analog_simulation`. - -## Built-in models (quick reference) - -| Model | Entry point | Local dimension per site | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | -| Transverse-field Ising | {meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.ising` | 2 (qubits) | -| Heisenberg | {meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.heisenberg` | 2 | -| Generic Pauli sums | {meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.pauli` or {meth}`~mqt.yaqs.core.data_structures.mpo.MPO.from_pauli_sum` | 2 | -| 1D Fermi–Hubbard | {meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.fermi_hubbard_1d` | 4 (fermionic) or 2 (Jordan–Wigner) | -| Bose–Hubbard | {meth}`~mqt.yaqs.core.data_structures.mpo.MPO.bose_hubbard` → `Hamiltonian.from_mpo` | `local_dim` (boson occupation cutoff) | -| Coupled transmon chain | {meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.coupled_transmon` | alternating qubit / resonator dims | -| Trapped ion (position grid) | {meth}`~mqt.yaqs.core.data_structures.mpo.MPO.trapped_ion` → `Hamiltonian.from_mpo` | grid points per ion (1–2 ions) | - -Open (`bc="open"`) and periodic (`bc="periodic"`) boundaries are supported on -the Pauli builders. - -## Pauli-string Hamiltonians - -### Ising model (shortcut) - -The transverse-field Ising Hamiltonian on an open chain is +This operator is $0.4Z_0Z_3+0.2Y_1+0.1I$. Site indices start at zero; omitted +sites act as identities, and an empty string denotes the identity operator. +Labels `I`, `X`, `Y`, and `Z` are case-insensitive. Use real coefficients for +Hermitian Pauli terms. -```{math} -H = -J \sum_i Z_i Z_{i+1} - g \sum_i X_i. -``` +## Use other local dimensions -```{code-cell} ipython3 -from mqt.yaqs import Hamiltonian +Hubbard and device models need an initial state with the same local dimensions +as the Hamiltonian. For a uniform layout, use `physical_dimensions=local_dim`; +for different dimensions, supply a list in site order. The +{doc}`state_initialization` guide explains these preparations. -L = 4 -J, g = 1.0, 0.5 -H_ising = Hamiltonian.ising(L, J, g) -``` +The coupled-transmon builder alternates transmons and resonators, starting with +a transmon. Its `length` counts both kinds of sites. The trapped ion builder +uses one site per ion, with a local dimension equal to the number of grid +points. See {doc}`transmon_emulation` and {doc}`trapped_ion` for worked device +examples, noise, and the relevant units. -### Structured one- and two-body Pauli terms +:::{dropdown} Fermi–Hubbard: physical sites and Jordan–Wigner orbitals -{meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.pauli` expands -nearest-neighbour `two_body` and on-site `one_body` lists into Pauli strings -automatically: +The default builder uses dimension-four sites with local basis $|0\rangle$, +$|\!\downarrow\rangle$, $|\!\uparrow\rangle$, $|\!\uparrow\downarrow\rangle$: -```{code-cell} ipython3 -H_ising = Hamiltonian.pauli( - length=L, - two_body=[(-J, "Z", "Z")], - one_body=[(-g, "X")], - bc="open", -) +```python +num_sites = 3 +fermi = Hamiltonian.fermi_hubbard_1d(num_sites, t=1.0, u=0.5) +fermi_state = State(num_sites, physical_dimensions=4) ``` -The Heisenberg model is available as a one-liner as well: +This mode uses ladder operators on composite sites. For a Pauli-chain model with +full Jordan–Wigner signs between spin orbitals, set `jordan_wigner=True`: -```{code-cell} ipython3 -H_heisenberg = Hamiltonian.heisenberg(L, Jx=1.0, Jy=1.0, Jz=1.0, h=0.2) +```python +jw = Hamiltonian.fermi_hubbard_1d(2 * num_sites, t=1.0, u=0.5, jordan_wigner=True) +jw_state = State(2 * num_sites) ``` -### Explicit Pauli strings (`from_pauli_sum`) +The two modes use different hopping-sign conventions. Match both the state and +operator basis when comparing them. -For **arbitrary** Pauli strings—including long-range couplings—pass -`(coefficient, spec)` pairs to -{meth}`~mqt.yaqs.core.data_structures.mpo.MPO.from_pauli_sum`. Each `spec` lists -operators with **site indices**, e.g. `"Z0 Z3"` or `"X2"`: +Here, `length` counts spin orbitals and must be even and at least two. Site +order is $1\uparrow,1\downarrow,2\uparrow,2\downarrow,\ldots$. Both builders use +open boundaries and omit a chemical-potential term. The +{func}`~mqt.yaqs.core.libraries.circuit_library.create_1d_fermi_hubbard_circuit` +provides a digital Trotter circuit with a chemical-potential option. -```{code-cell} ipython3 -from mqt.yaqs import MPO +::: -terms = [(-J, f"Z{i} Z{i+1}") for i in range(L - 1)] + [(-g, f"X{i}") for i in range(L)] -mpo = MPO() -mpo.from_pauli_sum(terms=terms, length=L) -H_custom = Hamiltonian.from_mpo(mpo) -``` +:::{dropdown} Bose–Hubbard and the occupation cutoff -Long-range terms are ordinary entries in `terms`: +`local_dim` retains occupations from zero through `local_dim - 1` at each site. +The builder includes an on-site frequency, an interaction $U n_i(n_i-1)/2$, and +nearest-neighbor hopping with coefficient $-J$: ```python -terms.append((0.1, "Z0 Z3")) # Z on sites 0 and 3 +local_dim = 3 +bose = Hamiltonian.from_mpo( + MPO.bose_hubbard( + length=3, + local_dim=local_dim, + omega=1.0, + hopping_j=0.2, + hubbard_u=0.5, + ) +) +bose_state = State(3, physical_dimensions=local_dim) ``` -Pauli labels are `I`, `X`, `Y`, `Z` (case-insensitive). Only -`physical_dimension=2` is supported for this builder. +Choose a large enough occupation cutoff for your preparation and dynamics. Check +convergence by increasing `local_dim` when higher occupations matter. -## Fermi–Hubbard (1D) +::: -{meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.fermi_hubbard_1d` -implements +## Supply a dense or sparse matrix -```{math} -H = -t \sum_{i,\sigma} \left(c^\dagger_{i,\sigma} c_{i+1,\sigma} + \mathrm{h.c.}\right) -+ U \sum_i n_{i,\uparrow} n_{i,\downarrow} -``` +For a small custom operator, pass exactly one of `matrix`, `sparse_matrix`, or +`tensors`. Dense and sparse matrices must be finite, square, and Hermitian. The +manual constructor uses a uniform `physical_dimension`, defaulting to two; it +infers `length` from the matrix size when omitted. -(open boundaries, no chemical potential). +(physical-site-ordering)= -### Fermionic sites (default) +### Physical-site ordering -One **physical site** has local dimension 4 with basis -$|0\rangle, |\!\downarrow\rangle, |\!\uparrow\rangle, |\!\uparrow\downarrow\rangle$. -Ladder operators act on the composite ↑/↓ space per site (not a Jordan–Wigner -qubit chain across sites). +Dense and sparse matrices use site 0 as the least-significant, fastest-varying +subsystem, matching Qiskit's qubit ordering. An operator acting on site 0 is +therefore the rightmost Kronecker factor: -```{code-cell} ipython3 -num_sites = 4 -t, u = 1.0, 0.5 +```{code-cell} python +import numpy as np +from scipy.sparse import csr_matrix -H_fermi = Hamiltonian.fermi_hubbard_1d(num_sites, t=t, u=u) -``` +identity = np.eye(2, dtype=complex) +pauli_x = np.array([[0, 1], [1, 0]], dtype=complex) +x_on_site_0 = np.kron(identity, pauli_x) -Pair with {class}`~mqt.yaqs.core.data_structures.state.State` using -`physical_dimensions=[4] * num_sites` when building product Fock states (see -{doc}`state_initialization`). +dense = Hamiltonian(matrix=x_on_site_0) +sparse = Hamiltonian(sparse_matrix=csr_matrix(x_on_site_0)) +``` -### Jordan–Wigner Pauli chain +For local dimensions $d_0,d_1,\ldots$, the flat index for basis digits +$(s_0,s_1,\ldots)$ is $s_0+d_0s_1+d_0d_1s_2+\cdots$. This order applies to full +spatial state vectors and operator matrices. Local observable and noise matrices +use the order of their explicit site list: for example, +`Observable(np.kron(X, Z), sites=[0, 1])` means $X_0Z_1$. Custom adjacent +two-site noise matrices require ascending site lists. Circuit matrices follow +Qiskit's gate convention. -Pass `jordan_wigner=True` for a qubit chain in the order 1↑, 1↓, 2↑, 2↓, … Here -`length` is the number of **spin orbitals** (must be even): +The initial state's representation selects the analog backend, independently of +the Hamiltonian's source data. See {doc}`representation_comparison` for the +supported choices. -```{code-cell} ipython3 -num_orbitals = 2 * num_sites -H_jw = Hamiltonian.fermi_hubbard_1d(num_orbitals, t=t, u=u, jordan_wigner=True) +```{warning} +Converting a sparse matrix to an MPO densifies the full operator. A sparse +source therefore does not avoid the full matrix allocation when you run with an +MPS state. For large MPS simulations, build an MPO directly with a preset, Pauli +terms, or custom tensors. ``` -Use this mode when you need Pauli-string semantics with full JW signs between -orbitals. - -```{note} -The analog MPO factories omit a chemical potential $\mu$. For a **digital** -Trotter circuit with $\mu$, see -{func}`~mqt.yaqs.core.libraries.circuit_library.create_1d_fermi_hubbard_circuit` -and {doc}`circuit_observables`. -``` +## Time-dependent Hamiltonians -Correctness of the fermionic and JW MPOs is covered by `test_fermi_hubbard_1d_*` -in the package test suite. +Use `Hamiltonian.piecewise` for an analog quench: evolve under one static +Hamiltonian and then another, on a shared time grid. Every duration must be a +positive integer multiple of `dt`, and their sum must equal `elapsed_time`. All +pieces must have matching site counts and local dimensions: -## Bose–Hubbard +```{code-cell} python +from mqt.yaqs import AnalogSimParams, Observable, Simulator -The Bose–Hubbard model +quench = Hamiltonian.piecewise([ + (hamiltonian, 0.2), + (Hamiltonian.ising(length, J=1.0, g=2.0), 0.2), +]) +params = AnalogSimParams( + observables=[Observable("z", 0)], elapsed_time=quench.duration, dt=0.05 +) -```{math} -H = \sum_i \left(\omega\, n_i + \frac{U}{2}\, n_i(n_i-1)\right) -- J \sum_i \left(a^\dagger_i a_{i+1} + \mathrm{h.c.}\right) +sim = Simulator(show_progress=False) +result = sim.run(state, quench, params) ``` -is available on {meth}`~mqt.yaqs.core.data_structures.mpo.MPO.bose_hubbard`. -Wrap the MPO for analog simulation: - -```{code-cell} ipython3 -from mqt.yaqs import Hamiltonian, MPO +The result contains nine sample times, including time zero, on one continuous +timeline. This path supports a single MPS state and the default TDVP evolution; +it also accepts a shared noise model. It does not support dense state +representations, BUG evolution, or list-of-state ensembles. -local_dim = 3 # occupations 0, 1, …, local_dim - 1 -H_bh = Hamiltonian.from_mpo( - MPO.bose_hubbard( - length=3, - local_dim=local_dim, - omega=1.0, - hopping_j=0.2, - hubbard_u=0.5, - ) -) -``` +For digital gates, different time steps, or segment-specific noise, use a +`SimulationProgram`; see {doc}`digital_analog_simulation`. A piecewise +Hamiltonian has no single static MPO or matrix. Select a static entry from +`quench.pieces` when you need its operator. -Initial states must respect the boson dimension, e.g. -`State(length, initial="zeros", physical_dimensions=[local_dim] * length)`. +## Advanced operator use -## Coupled transmon–resonator chains +:::{dropdown} Custom MPO tensors and construction accuracy -{meth}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian.coupled_transmon` -builds an alternating chain of transmon qubits and resonators with local -dimensions `qubit_dim` and `resonator_dim`: +Manual cores use `(left, right, physical_out, physical_in)` axes in ascending +site order. Neighboring bonds must match, exterior bonds must have dimension +one, and entries must be finite. The manual `Hamiltonian` constructor requires +uniform local dimensions. For example, these bond-one cores build the same $X_0$ +operator as the matrix example: -```{code-cell} ipython3 -H_transmon = Hamiltonian.coupled_transmon( - length=3, - qubit_dim=3, - resonator_dim=5, - qubit_freq=5.0, - resonator_freq=7.0, - anharmonicity=-0.3, - coupling=0.1, +```python +tensor_hamiltonian = Hamiltonian( + tensors=[ + pauli_x.reshape(1, 1, 2, 2), + identity.reshape(1, 1, 2, 2), + ] ) ``` -For excitation transfer through a resonator, including relaxation and dephasing, -see {doc}`transmon_emulation`. +When you already have an MPO, use `Hamiltonian.from_mpo`. It references the same +MPO, checks its structure, and takes its local dimensions from the cores. +Wrapped MPOs and tensor inputs must represent a globally Hermitian operator; +YAQS does not perform a full-matrix Hermiticity check for these inputs. -## Trapped ion position grid +Pauli builders expose `tol`, `max_bond_dim`, and `n_sweeps` for operator +compression. Dense-to-MPO conversion also uses an SVD cutoff, so the cached MPO +can approximate the source matrix. Set conversion options explicitly with +`MPO.from_matrix(matrix, d=2, cutoff=..., max_bond=...)` and wrap the result +when you need control over this approximation. These choices concern the +operator itself; simulation-parameter presets control the evolving state. -{meth}`~mqt.yaqs.core.data_structures.mpo.MPO.trapped_ion` builds a **static** -Hamiltonian for one or two ions on a uniform position grid. Each ion is one MPO -site with local dimension equal to the number of grid points. The local terms -are a harmonic trap plus a centered finite-difference kinetic energy; for two -ions, a softened Coulomb repulsion is compressed into MPO channels (optional SVD -truncation via `coulomb_cutoff` or `max_bond_dim`). +Static Hamiltonians cache converted forms. Call `ensure_mpo()` before reading +`.mpo`, or `ensure_sparse()` before reading `.sparse_matrix` if the form is not +already available. `to_matrix()` and `to_sparse_matrix()` return full operators +for small-system inspection. Create a new Hamiltonian when changing the +operator; cached forms do not track edits to the source data. -```{math} -H = \sum_i \left[-\frac{\hbar^2}{2m_i}\frac{d^2}{dx_i^2} + \tfrac{1}{2} m_i \omega^2 (x_i - q)^2\right] -+ \frac{g}{\sqrt{(x_1-x_2)^2 + a^2}} -``` - -(the Coulomb term applies only when two masses are supplied). +::: -```{code-cell} ipython3 -from mqt.yaqs import Hamiltonian, MPO -import numpy as np - -positions = np.linspace(-6.0, 6.0, 25) -H_ion = Hamiltonian.from_mpo( - MPO.trapped_ion( - positions, - masses=[1.0], - omega=1.0, - trap_center=0.0, - ) -) - -# Two ions with softened Coulomb repulsion on the same grid spacing -H_pair = Hamiltonian.from_mpo( - MPO.trapped_ion( - positions, - masses=[1.0, 1.0], - omega=1.0, - coulomb_strength=1.0, - softening_length=float(positions[1] - positions[0]), - ) -) -``` +:::{dropdown} Energy and long-range correlations of an MPS -Pair with {class}`~mqt.yaqs.core.data_structures.state.State` using -`physical_dimensions=[len(positions)]` per ion site. For wavepacket oscillation, -trap transport, and heating from random momentum kicks, see {doc}`trapped_ion`. +Contract an MPS with a static Hamiltonian's MPO to obtain its energy. For an +arbitrary nonzero state, divide the raw contraction by the squared norm: -```{note} -YAQS applies $\exp(-\mathrm{i}\,\Delta t\, H)$ during evolution. When using SI -units, pass energies and times in consistent units or rescale $H/\hbar$ -explicitly (see the factory docstring). +```python +hamiltonian.ensure_mpo() +norm_squared = state.mps.norm() ** 2 +energy = state.mps.expect_mpo(hamiltonian.mpo) / norm_squared ``` -## Manual Hamiltonians +`expect_mpo` returns the raw complex contraction without normalizing the state. +It uses the cached MPO, including any construction approximation. A Hermitian +energy is real up to numerical error. -For imported MPO cores or small-system dense/sparse operators: +The same method evaluates a full-chain Pauli product on separated sites: ```python -# MPO cores in ascending site order with (left, right, output, input) axes -# — preferred for TJM -H = Hamiltonian(tensors=my_cores) - -# Dense or sparse matrix — YAQS converts to MPO or sparse as needed at run time. -# sparse_matrix → TJM densifies; prefer tensors=/from_mpo/presets for large systems. -H = Hamiltonian(matrix=dense_h, physical_dimension=2) -H = Hamiltonian(sparse_matrix=sparse_h, physical_dimension=2) +correlation = MPO() +correlation.from_pauli_sum(terms=[(1.0, "Z0 Z3")], length=length, n_sweeps=0) +zz = state.mps.expect_mpo(correlation) / norm_squared +z0 = state.mps.expect(Observable("z", 0)) / norm_squared +z3 = state.mps.expect(Observable("z", 3)) / norm_squared +connected = zz - z0 * z3 ``` -(physical-site-ordering)= - -### Physical-site ordering +Here, `zz` is $\langle Z_0Z_3\rangle$ and `connected` subtracts +$\langle Z_0\rangle\langle Z_3\rangle$. Omitted sites act as identities. Use +`MPO.from_local_ops` instead when you already have one local matrix per site, +including identities between separated factors. -YAQS uses one mixed-radix order for full spatial states and operators. Site 0 is -the least-significant, fastest-varying subsystem. For local dimensions -$d_0,d_1,\ldots$, the basis digits $(s_0,s_1,\ldots)$ have flat index -$s_0+d_0s_1+d_0d_1s_2+\cdots$. For qubits, this is Qiskit's little-endian order. -The convention applies to dense `State` arrays, `MPS.to_vec()`, dense and sparse -`Hamiltonian` inputs, MPO matrix conversions, and `EquivalenceChecker` matrices. -A full operator on site 0 is therefore the rightmost Kronecker factor. For -example, `np.kron(I, X)` applies `X` to site 0 of a two-site system: - -```{code-cell} ipython3 -identity = np.eye(2, dtype=np.complex128) -pauli_x = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex128) -x_on_site_0 = np.kron(identity, pauli_x) - -manual = Hamiltonian(matrix=x_on_site_0) -np.testing.assert_allclose(manual.to_matrix(), x_on_site_0) -np.testing.assert_allclose(manual.to_sparse_matrix().toarray(), x_on_site_0) -manual.ensure_mpo() -np.testing.assert_allclose(manual.mpo.to_matrix(), x_on_site_0) -np.testing.assert_allclose(manual.mpo.to_sparse_matrix().toarray(), x_on_site_0) -``` +::: -Local observable and analog-noise matrices use their explicit site list instead. -The first matrix tensor factor acts on the first listed site. Thus -`Observable(np.kron(X, Z), sites=[0, 1])` means $X_0Z_1$; with `sites=[1, 0]`, -it means $X_1Z_0$. YAQS permutes local matrix legs when it embeds such an -operator into the full site-0-LSB space. Two-qubit observables support -nearest-neighbor pairs and the periodic pair `{0, L - 1}` in either site order -on all analog representations. Named two-site noise processes retain the letter -order of the supplied sites when YAQS normalizes a reversed site list. Custom -adjacent two-site noise matrices require ascending sites. Circuit gate matrices -instead retain Qiskit's qarg and matrix convention. This distinction lets each -local API keep its declared tensor-factor meaning while every full matrix uses -one global basis order. - -## Related topics - -- {doc}`analog_simulation` — TJM evolution, noise, and observables -- {doc}`digital_analog_simulation` — analog evolution mixed with digital gates -- {doc}`transmon_emulation` — multi-level transmon physics -- {doc}`trapped_ion` — position-grid wavepacket dynamics -- {doc}`state_initialization` — `physical_dimensions` and representations -- {doc}`simulation_parameters` — truncation presets for MPO evolution +With the operator and initial state prepared, continue with +{doc}`analog_simulation` for noisy dynamics or {doc}`simulation_parameters` for +accuracy and sampling choices. The {class}`~mqt.yaqs.Hamiltonian` API reference +lists the full constructor signatures. diff --git a/docs/examples/realistic_noise_models.md b/docs/examples/realistic_noise_models.md index 1d11a43ac..8e83916f1 100644 --- a/docs/examples/realistic_noise_models.md +++ b/docs/examples/realistic_noise_models.md @@ -2,363 +2,366 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true - execution_timeout: 300 + execution_timeout: 120 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` +# Noise Models + +A {class}`~mqt.yaqs.NoiseModel` describes how a system loses energy, gains +excitations, or suffers other disturbances during a simulation. Assemble named +jump operators or supply your own matrices, then pass the model to +`Simulator.run`. Use fixed strengths for a calibrated model, or distributions to +represent static variation between runs. + +## Define the noise processes + +Each process has a `name`, a list of `sites`, and a `strength`. Site indices +start at zero. This four-qubit model combines local relaxation and dephasing: -# 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 {class}`~mqt.yaqs.core.data_structures.noise_model.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 {meth}`~mqt.yaqs.Simulator.run` -starts; all trajectories in that run share the same sampled disorder. The -realized model is stored on {attr}`~mqt.yaqs.Result.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 -{class}`~mqt.yaqs.core.libraries.noise_library.NoiseLibrary`. - -Malformed configs raise `TypeError` (wrong types, including booleans used as -sites/strengths) or `ValueError` (invalid values). Fixed `strength` values must -be finite and **nonnegative**—they are Lindblad/quantum-jump rates $\gamma$, not -signed dissipator coefficients. Temporarily negative rates can appear in -time-local non-Markovian master equations, but YAQS does not implement signed -generators or reverse-jump unravelings. Custom `matrix` / `factors` entries may -still contain negative elements. Site lists/tuples must contain exactly one or -two distinct nonnegative integers. Custom adjacent two-site matrices require -**ascending** site order. To convert a matrix written for descending sites, -reverse the site list and swap the matrix's input and output tensor-factor axes. -Use `factors` only for non-adjacent pairs. - -This rate interpretation applies to `Simulator`. For sampled equivalence -checking, `EquivalenceChecker` instead treats resolved strengths as direct -per-opportunity branch probabilities; see {ref}`equivalence-noise-model`. - -```{code-cell} ipython3 +```{code-cell} python from mqt.yaqs import NoiseModel -L = 4 -processes = [ - {"name": "lowering", "sites": [i], "strength": 0.05} for i in range(L) +length = 4 +noise = NoiseModel([ + {"name": "lowering", "sites": [site], "strength": 0.1} + for site in range(length) ] + [ - {"name": "pauli_z", "sites": [i], "strength": 0.02} for i in range(L) -] + [ - {"name": "crosstalk_xx", "sites": [i, i + 1], "strength": 0.01} for i in range(L - 1) -] + {"name": "pauli_z", "sites": [site], "strength": 0.02} + for site in range(length) +]) +``` -noise_model = NoiseModel(processes) +The named operators act on qubits. Names are case-sensitive: + +| Process name | Effect | Sites | +| ------------------------------------- | --------------------------------------------------------------------- | --------------------------------- | +| `"lowering"` | Relaxation from $\lvert1\rangle$ to $\lvert0\rangle$. | One. | +| `"raising"` | Excitation from $\lvert0\rangle$ to $\lvert1\rangle$. | One. | +| `"pauli_x"`, `"pauli_y"`, `"pauli_z"` | Pauli errors; $Z$ causes dephasing. Aliases: `"x"`, `"y"`, `"z"`. | One. | +| `"lowering_two"`, `"raising_two"` | Joint relaxation $\lvert11\rangle\to\lvert00\rangle$, or the reverse. | Two adjacent sites. | +| `"crosstalk_xx"`, `"crosstalk_xy"`, … | Correlated Pauli errors; any pair of `x`, `y`, and `z`. | Two; see the support table below. | + +Joint relaxation is one two-site jump. To model independent relaxation on two +sites, supply two `"lowering"` processes instead. + +## Interpret the strengths + +For `Simulator`, `strength` is a finite, nonnegative Lindblad rate $\gamma$. Use +inverse units of the simulation time. YAQS supplies the factor $\sqrt{\gamma}$; +pass the unscaled operator as `matrix`. Operators need not be Hermitian or +unitary, and YAQS does not normalize them. + +Analog noise acts over the physical time steps set by `AnalogSimParams.dt`. +Circuit noise uses a unit noise step after each multi-qubit gate, with only the +processes whose sites lie within that gate's qubits. Single-qubit gates, +barriers, and idle qubits do not create noise opportunities. Circuit strengths +therefore do not represent hardware gate durations or direct error +probabilities. See {doc}`circuit_observables` for a worked example. + +:::{important} +`EquivalenceChecker` interprets resolved strengths as direct per-opportunity +branch probabilities and imposes its own probability constraints. A simulator +rate cannot be reused as a checker probability without choosing a conversion. +See {ref}`equivalence-noise-model`. +::: + +:::{dropdown} Relate rates to relaxation and dephasing times +For a jump operator $L$, the rate multiplies the dissipator + +$$ +\gamma\mathcal{D}[L](\rho)=\gamma\left( +L\rho L^\dagger-\tfrac12\{L^\dagger L,\rho\}\right). +$$ + +With only `"lowering"` and no Hamiltonian, the excited-state population decays +as $e^{-\gamma t}$, so $\gamma=1/T_1$. With only `"pauli_z"`, the off-diagonal +density-matrix entries decay as $e^{-2\gamma t}$, so $\gamma=1/(2T_\phi)$. These +conventions matter when converting measured lifetimes to strengths. Scaling $L$ +by a factor $c$ scales its dissipator by $|c|^2$. + +Negative rates, including time-local descriptions with temporarily negative +coefficients, are not supported. Negative or complex matrix entries remain valid +parts of a jump operator. +::: + +## Run and inspect the model + +Pass the model with the initial state, Hamiltonian, and simulation parameters. +This short Ising evolution uses the default MPS representation and averages +eight trajectories: + +```{code-cell} python +from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State + +state = State(length, initial="ones") +hamiltonian = Hamiltonian.ising(length, J=1.0, g=0.5) +params = AnalogSimParams( + observables=[Observable("z", site) for site in range(length)], + elapsed_time=0.2, + dt=0.05, + num_traj=8, + random_seed=7, +) + +sim = Simulator(show_progress=False) +result = sim.run(state, hamiltonian, params, noise_model=noise) +print(result.expectation_values[0].shape) ``` -## 2. Log-normal disorder on strengths +The shape is `(5,)`: five sampled times, including the initial time. +`result.expectation_values` contains one such array per observable, in the +supplied order. Parallel execution remains enabled; the documentation hides +progress bars. Eight trajectories suffice to demonstrate the call, but +scientific results need a convergence check. The model used in the run is +available as `result.noise_model`. -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. +## Represent static variation -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$: +Replace a scalar strength with a distribution dictionary. This example gives +each qubit an independent log-normal relaxation rate: -```{code-cell} ipython3 -bell_curve_strength = {"distribution": "lognormal", "mean": -2.3, "std": 0.5} +```{code-cell} python +import numpy as np -disordered_processes = [ +variable_noise = NoiseModel([ { - "name": "pauli_z", - "sites": [i], - "strength": bell_curve_strength, + "name": "lowering", + "sites": [site], + "strength": {"distribution": "lognormal", "mean": np.log(0.1), "std": 0.4}, } - for i in range(L) -] + for site in range(length) +]) -disordered_model = NoiseModel(disordered_processes) +resolved_noise = variable_noise.sample(rng=7) +print([process["strength"] for process in resolved_noise.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: - -```{code-cell} ipython3 -import matplotlib.pyplot as plt -import matplotlib.ticker as mticker -import numpy as np -from scipy import stats - -rng = np.random.default_rng(0) -samples = [disordered_model.sample(rng=rng).processes[0]["strength"] for _ in range(5000)] - -mu = bell_curve_strength["mean"] -sigma = bell_curve_strength["std"] -x = np.logspace(np.log10(min(samples)), np.log10(max(samples)), 200) -pdf = stats.lognorm.pdf(x, s=sigma, scale=np.exp(mu)) - -fig, ax = plt.subplots(figsize=(7, 3.8), layout="constrained") -ax.hist(samples, bins=40, density=True, alpha=0.7, color="tab:blue", label="sampled strengths") -ax.plot(x, pdf, color="black", lw=1.5, label="log-normal pdf") -ax.set_xscale("log") - -# Sparse decade ticks with plain decimal labels (avoids crowded sci-notation on log axes) -lo, hi = float(min(samples)), float(max(samples)) -tick_decades = np.arange(int(np.floor(np.log10(lo))), int(np.ceil(np.log10(hi))) + 1) -tick_candidates = np.concatenate([np.array([1, 2, 5]) * 10.0**e for e in tick_decades]) -ticks = tick_candidates[(tick_candidates >= lo * 0.9) & (tick_candidates <= hi * 1.1)] -if len(ticks) > 6: - ticks = ticks[np.linspace(0, len(ticks) - 1, 6, dtype=int)] -ax.set_xticks(ticks) -ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:g}")) -ax.xaxis.set_minor_locator(mticker.NullLocator()) - -ax.set_xlabel("sampled dephasing strength") -ax.set_ylabel("density") -ax.set_title("Log-normal disorder (median ≈ {:.3f})".format(np.exp(mu))) -ax.legend() -ax.grid(alpha=0.3, which="both") -plt.show() -``` +{meth}`~mqt.yaqs.NoiseModel.sample` returns a new model with concrete rates; it +leaves the original model unchanged. For `"lognormal"`, `mean` and `std` +describe the normal distribution of $\log\gamma$, so the median rate above is +`0.1`. -## 3. Disorder in an analog simulation +| `distribution` | Meaning of `mean` and `std` | Treatment of negative draws | +| -------------------- | ---------------------------------------------------------- | --------------------------------------------------------------- | +| `"lognormal"` | Mean and standard deviation of $\log\gamma$. | All draws are positive. | +| `"normal"` | Mean and standard deviation of a normal rate distribution. | Clamped to zero with a warning. | +| `"truncated_normal"` | Parameters of the underlying normal distribution. | Sampled from that distribution restricted to nonnegative rates. | -We evolve a short Ising chain from a Néel product state and compare: +Passing `variable_noise` directly to `Simulator.run` draws one rate per process +at the start of the run. All trajectories share those rates. This represents +static disorder: rates do not change during the evolution or between +trajectories. To average over disorder, perform separate runs with separate +draws; increasing `num_traj` only improves the trajectory average for one draw. -- **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. +To compare setups using the same rates, pass a resolved model: -```{code-cell} ipython3 -from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State +```{code-cell} python +resolved_result = sim.run(state, hamiltonian, params, noise_model=resolved_noise) +print([process["strength"] for process in resolved_result.noise_model.processes]) +``` -# Wider log-normal spread for a visible disorder effect in dynamics -dyn_strength = {"distribution": "lognormal", "mean": -0.7, "std": 1.0} -dyn_disordered = NoiseModel([ - {"name": "pauli_z", "sites": [i], "strength": dyn_strength} for i in range(L) +The printed rates match the earlier sample. You can also reuse a previous run's +`result.noise_model`. Setting `params.random_seed` fixes automatic disorder +draws and trajectory random streams for a fixed setup. It does not seed +independently prepared random states or final shot sampling, or guarantee +identical numbers across software versions and platforms. For successive manual +disorder draws, reuse a NumPy `Generator`; repeating `.sample(rng=7)` repeats +the same draw. + +:::{dropdown} Distribution parameters and limits +`mean` and `std` default to zero when omitted; specify both to make the intended +distribution clear. `mean` must be finite, and `std` must be finite and +nonnegative. A zero-width normal or truncated normal resolves to `max(0, mean)`; +a zero-width log-normal resolves to `exp(mean)`. + +There is no default distribution or upper rate limit. Choose a distribution from +the variation you intend to model, and choose the time step to resolve the +resulting dynamics. Clamping a normal distribution creates a point mass at zero; +truncating it renormalizes the positive part instead. Neither choice creates a +time-dependent noise model. +::: + +(noise-custom-operators)= + +## Supply a custom operator + +Add `matrix` to override the library lookup; `name` then serves as an +identifier. A one-site operator must be a finite, square matrix matching that +site's local dimension. This explicit matrix gives the same jump as +`"lowering"`: + +```{code-cell} python +sigma_minus = np.array([[0, 1], [0, 0]], dtype=complex) +custom_noise = NoiseModel([ + {"name": "relaxation", "sites": [0], "strength": 0.1, "matrix": sigma_minus}, + {"name": "pauli_z", "sites": [1], "strength": 0.02}, ]) +``` -hamiltonian = Hamiltonian.ising(length=L, J=1.0, g=0.5) -state = State(L, initial="Neel") -z_obs = Observable("z", sites=0) +You can mix custom and named processes. For higher local dimensions, supply an +operator in that local basis. A truncated oscillator with levels $0,1,2$ uses +the annihilation operator -sim_params = AnalogSimParams( - observables=[z_obs], - elapsed_time=8.0, - dt=0.1, - num_traj=32, - max_bond_dim=24, - random_seed=7, -) - -median_strength = float(np.exp(dyn_strength["mean"])) -baseline_model = NoiseModel([ - {"name": "pauli_z", "sites": [i], "strength": median_strength} for i in range(L) +```{code-cell} python +annihilation = np.diag(np.sqrt(np.arange(1, 3)), k=1) +qutrit_noise = NoiseModel([ + {"name": "loss", "sites": [0], "strength": 0.1, "matrix": annihilation}, ]) - -sim = Simulator(show_progress=False) -result_baseline = sim.run(state, hamiltonian, sim_params, baseline_model) -result_disordered = sim.run(state, hamiltonian, sim_params, dyn_disordered) - -# Ensemble of disorder realizations for a shaded band (keep small for doc build time) -ensemble_curves = [] -for seed in range(8, 12): - params_i = AnalogSimParams( - observables=[z_obs], - elapsed_time=8.0, - dt=0.1, - num_traj=16, - max_bond_dim=24, - random_seed=seed, - ) - res_i = sim.run(state, hamiltonian, params_i, dyn_disordered) - ensemble_curves.append(res_i.expectation_values[0]) -ensemble_curves = np.asarray(ensemble_curves) ``` -```{code-cell} ipython3 ---- -mystnb: - image: - width: 80% - align: center ---- -import matplotlib.pyplot as plt -import matplotlib.ticker as mticker - -times = sim_params.times -baseline_curve = result_baseline.expectation_values[0] -disordered_curve = result_disordered.expectation_values[0] - -fig, ax = plt.subplots(figsize=(7, 4), layout="constrained") -ax.fill_between( - times, - ensemble_curves.min(axis=0), - ensemble_curves.max(axis=0), - color="tab:orange", - alpha=0.25, - label="disordered ensemble (4 seeds)", -) -ax.plot(times, baseline_curve, label="fixed median strength", color="black", linestyle="--", lw=2) -ax.plot(times, disordered_curve, label="one disordered sample", color="tab:orange", lw=1.5) -ax.set_xlabel("time") -ax.set_ylabel(r"$\langle Z_0 \rangle$") -ax.set_title("Log-normal static disorder shifts open-system decay") -ax.xaxis.set_major_locator(mticker.MaxNLocator(6)) -ax.legend() -ax.grid(alpha=0.3) -plt.show() -``` +This matrix is $3\times3$ and requires a dimension-three site. See +{doc}`transmon_emulation` for a device example with such operators. + +## Choose a supported combination -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. +The state representation selects the analog backend. One-site and adjacent +two-site process matrices work with all three analog representations. Long-range +processes need the choices below: -## 4. Disorder on a noisy circuit +| Workflow | One-site and adjacent two-site noise | Non-adjacent two-site noise | +| -------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------- | +| Analog MPS (TJM) | Supported. | Products of Pauli operators, up to a unit-modulus phase per factor. | +| Analog vector (MCWF) | Supported. | Custom factor pairs supported. | +| Analog density matrix (Lindblad) | Supported. | Custom factor pairs supported. | +| Circuit simulation (MPS) | Supported on the gate qubits at each noise opportunity. | Not supported. | -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. +All operators must match the site's local dimension. Named Pauli operators are +$2\times2$; supply custom matrices for other dimensions. YAQS checks dimensions +and site bounds against the state when the simulation starts. See +{doc}`representation_comparison` to choose a representation. -```{code-cell} ipython3 -from mqt.yaqs import Observable, DigitalSimParams -from mqt.yaqs.core.libraries.circuit_library import create_ising_circuit +Noisy MPS, vector, and circuit runs cannot retain a final pure state with +`get_state=True`; noisy density-matrix runs can retain their final mixed state. +The analog `list[State]` ensemble workflow does not support process noise. See +{doc}`simulation_parameters` for output choices. -num_qubits = 3 -circuit = create_ising_circuit(L=num_qubits, J=1.0, g=0.5, dt=0.1, timesteps=5) +:::{dropdown} Construct adjacent and long-range two-site processes +For adjacent sites, supply a `matrix` acting on their joint basis. Use ascending +site order: the first tensor factor acts on the first listed site. If a matrix +was written for descending sites, swap both its input and output tensor-factor +axes as well as reversing the site list. -circuit_noise = NoiseModel([ +```python +adjacent_noise = NoiseModel([ { - "name": "pauli_x", - "sites": [i], - "strength": {"distribution": "lognormal", "mean": -3.0, "std": 0.4}, - } - for i in range(num_qubits) + "name": "joint_relaxation", + "sites": [0, 1], + "strength": 0.05, + "matrix": np.kron(sigma_minus, sigma_minus), + }, ]) - -circuit_params = DigitalSimParams( - observables=[Observable("z", site) for site in range(num_qubits)], - num_traj=32, - max_bond_dim=8, - random_seed=11, -) - -circuit_result = sim.run(State(num_qubits, initial="zeros"), circuit, circuit_params, circuit_noise) ``` -## 5. Long-range crosstalk +For non-adjacent sites, the names `"crosstalk_xy"` and +`"longrange_crosstalk_xy"` both construct the factors $X$ and $Y$. Any pair of +`x`, `y`, and `z` is accepted: -Non-adjacent pairs use the exact `longrange_crosstalk_[xyz]{2}` naming -convention (e.g. `longrange_crosstalk_xy`); YAQS attaches per-site Pauli factors -automatically. The same physics works on analog MPS TJM, MCWF, and Lindblad. -Digital TJM rejects non-adjacent / factorized two-site noise up front -(gate-local scoping remains nearest-neighbor only). - -```{code-cell} ipython3 -lr_model = NoiseModel([ - {"name": "longrange_crosstalk_xy", "sites": [0, 2], "strength": 0.05}, +```python +long_range_pauli = NoiseModel([ + {"name": "longrange_crosstalk_xy", "sites": [0, 3], "strength": 0.05}, ]) -sampled = lr_model.sample(rng=0) ``` -## 6. Custom jump operators - -Every noise process is a dictionary. Besides the built-in -{class}`~mqt.yaqs.core.libraries.noise_library.NoiseLibrary` names (`lowering`, -`pauli_x`, `crosstalk_xx`, …), you can supply your own operator as a NumPy -array: +For a custom non-adjacent process, supply two local `factors` in the order of +the listed sites. This lowering-and-dephasing product requires an analog vector +or density-matrix simulation: -| Key | Required | Description | -| ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | yes | Nonempty label. When `matrix`/`factors` are omitted, must be a library name, `x`/`y`/`z`, or exact `crosstalk_[xyz]{2}` / `longrange_crosstalk_[xyz]{2}`. | -| `sites` | yes | List/tuple of exactly one or two distinct nonnegative site indices. | -| `strength` | yes | Nonnegative finite rate $\gamma$, or a distribution dict (`normal` / `lognormal` / `truncated_normal`). | -| `matrix` | no | Square finite local operator $L$ (`d×d`, or `d_i d_j` for adjacent two-site). Ascending sites required for custom two-site matrices. | -| `factors` | no | Exactly two square one-site operators for non-adjacent pairs. | +```python +long_range_custom = NoiseModel([ + { + "name": "correlated_loss", + "sites": [0, 3], + "strength": 0.05, + "factors": (sigma_minus, np.diag([1, -1])), + }, +]) +``` -YAQS does not check complete positivity; supply physically meaningful jump -operators. The same `matrix` override works for **scheduled jumps** (see -{doc}`scheduled_jumps`) on supported backends and for process noise on TJM -(`mps`), MCWF (`vector`), Lindblad (`density_matrix`), and supported Simulator -digital-circuit runs. +YAQS sorts the sites and reorders the factors together. Use `matrix` for +adjacent sites and `factors` for non-adjacent sites; do not provide both. +::: -### Amplitude damping with an explicit $\sigma_-$ +(noise-scheduled-jumps)= -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: +## Apply a scheduled jump -```{code-cell} ipython3 -import numpy as np +Use `scheduled_jumps` for an operator applied at a specified analog time. Each +entry gives a `time`, `sites`, and a library `name`, or a custom `matrix` with +an identifying name. Scheduled events have no `strength`: the operator is +applied when the simulation reaches its time. -sigma_minus = np.array([[0, 1], [0, 0]], dtype=complex) +This four-site example isolates the event by setting the Hamiltonian to zero. An +$X$ flip at $t=0.1$ changes $\langle Z_0\rangle$ from $+1$ to $-1$: -custom_model = NoiseModel([ - {"name": "t1_explicit", "sites": [0], "strength": 0.1, "matrix": sigma_minus}, - {"name": "pauli_z", "sites": [1], "strength": 0.05}, +```{code-cell} python +scheduled_noise = NoiseModel(scheduled_jumps=[ + {"time": 0.1, "sites": [0], "name": "x"}, ]) +jump_state = State(length, initial="zeros") +zero_hamiltonian = Hamiltonian.ising(length, J=0.0, g=0.0) +jump_params = AnalogSimParams( + observables=[Observable("z", 0)], + elapsed_time=0.2, + dt=0.05, + num_traj=1, + order=1, +) +jump_result = sim.run(jump_state, zero_hamiltonian, jump_params, noise_model=scheduled_noise) +print(jump_result.expectation_values[0]) ``` -Run a short analog simulation using the custom operator: - -```{code-cell} ipython3 -from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State - -L2 = 2 -hamiltonian = Hamiltonian.ising(length=L2, J=1.0, g=0.5) -state = State(L2, initial="basis", basis_string="10") - -sim_params = AnalogSimParams( - observables=[Observable("z", sites=0), Observable("z", sites=1)], - elapsed_time=1.0, - dt=0.1, - num_traj=32, - max_bond_dim=8, - random_seed=3, +The five values are `[1, 1, -1, -1, -1]`, sampled at times +`[0, 0.05, 0.1, 0.15, 0.2]`. Event-only runs are deterministic, so one +trajectory suffices; they can also retain the final MPS with `get_state=True`. + +Scheduled jumps require a single MPS `State` and `AnalogSimParams(order=1)`. +MCWF, Lindblad, order-2 TJM, circuit runs, and `list[State]` ensembles do not +support them. Event times must lie on the simulation time grid, including its +endpoints. Two-site events must act on adjacent sites; custom matrices require +ascending site order. + +:::{important} +At a matching time after zero, scheduled operators replace the ordinary +stochastic-jump draw for that step; process dissipation still runs. For control +pulses that also sample stochastic noise at pulse times, use an +{doc}`analog-digital program `. +::: + +:::{dropdown} Custom scheduled operators and event order +Add `matrix` to supply a custom operator. For example, this schedules a $\pi/2$ +rotation about $Y$: + +```python +ry_pi2 = np.array([[1, -1], [1, 1]], dtype=complex) / np.sqrt(2) +custom_event = NoiseModel( + scheduled_jumps=[ + {"time": 0.1, "sites": [0], "name": "ry_pi2", "matrix": ry_pi2}, + ] ) - -result = 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 {doc}`transmon_emulation`. - -## Related topics - -- {doc}`analog_simulation` — TJM workflow with static noise strengths -- {doc}`circuit_observables` — digital circuit observables and mid-circuit - sampling -- {doc}`scheduled_jumps` — deterministic jumps at fixed times (library or custom - `matrix`) -- {doc}`representation_comparison` — MCWF and Lindblad backends with the same - `NoiseModel` -- {doc}`simulation_parameters` — presets and `random_seed` for reproducible - trajectories -- {doc}`quickstart` — minimal first simulation +Matrices must be finite, square, and match the selected sites. Scheduled events +accept `matrix`, not long-range `factors`. Operators need not be unitary; YAQS +normalizes the state after the events and rejects a zero or nonfinite norm. A +non-unitary scheduled operator is a prescribed normalized state update, not a +randomly sampled Lindblad channel. + +At time zero, events act before dissipation and the initial measurement. At +later grid points, including the final time, the order is Hamiltonian evolution, +process dissipation, all matching scheduled operators, normalization, then +measurement. Events at the same time follow their order in `scheduled_jumps`. +Inside a `SimulationProgram`, event times use the local clock of an analog run; +see {doc}`digital_analog_simulation` for segment boundaries and noise overrides. +::: + +## Next steps + +Use {doc}`analog_simulation` or {doc}`circuit_observables` to see how noise +changes measured dynamics. The device guides show how to combine noise with +{doc}`transmon_emulation` and {doc}`trapped_ion`. For deterministic control +sequences that combine gates with analog evolution, see +{doc}`digital_analog_simulation`. diff --git a/docs/examples/representation_comparison.md b/docs/examples/representation_comparison.md index 713be15bd..2a7ab389d 100644 --- a/docs/examples/representation_comparison.md +++ b/docs/examples/representation_comparison.md @@ -2,143 +2,286 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true - execution_timeout: 300 + execution_timeout: 120 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` +# State Representations + +The choice of state representation determines which solver YAQS uses and how +large a system you can study. Matrix product states keep large simulations +manageable when entanglement remains limited. Dense vectors and density matrices +provide useful small-system references, with different costs for noisy dynamics. + +Choose the representation when constructing a {class}`~mqt.yaqs.State`. YAQS +selects the solver from that choice; it does not switch representations when the +calculation becomes expensive. -# Representation Comparison +## Choose a representation and solver -YAQS supports multiple state **representations** for analog evolution. Each path -targets a different scaling regime; the table below summarizes when each is -appropriate. +| `State` representation | Analog solver | When to use it | +| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------------------ | +| `"mps"` (default) | Tensor jump method (TJM). | Large chains with manageable entanglement, and circuit simulation. | +| `"vector"` | Monte Carlo wave-function method (MCWF). | Small systems where you want pure-state trajectories without MPS compression. | +| `"density_matrix"` | Lindblad master equation. | Small open systems, mixed initial states, and deterministic ensemble averages. | -For how to set `representation` on -{class}`~mqt.yaqs.core.data_structures.state.State`, see -{doc}`state_initialization`. +For presets, use `State(length, initial="zeros", representation="vector")`, for +example. Supplying `vector=`, `density_matrix=`, or `tensors=` selects the +matching representation automatically. See {doc}`state_initialization` for state +preparation and local dimensions. -## Choosing a representation +With noise, TJM and MCWF evolve independent pure-state trajectories and average +their observables. Lindblad evolution propagates the ensemble density matrix +directly. Without noise, the trajectory solvers evolve a single pure state; +Lindblad evolution still carries a density matrix. -| Path | When to use | Notes | -| ------------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `"mps"` (default) | Larger systems and tensor-network-friendly Hamiltonians | TJM trajectories; tune `num_traj`, `max_bond_dim`, and {doc}`accuracy presets ` | -| `"vector"` | MCWF / state-vector quantum trajectories | Exponential memory in qubits; single-trajectory wavefunction dynamics | -| `"density_matrix"` | Lindblad master-equation evolution | Exponential memory; deterministic ensemble average without trajectory sampling | +## How state storage scales -Practical guidance: +Let $L$ be the number of sites, $d$ their common local dimension, and $D=d^L$ +the full Hilbert-space dimension. For qubits, $d=2$. An MPS stores local tensors +joined by bonds; the maximum bond dimension $\chi$ controls how much +entanglement it can represent. -- Start with `preset="balanced"` (or `"fast"` while exploring) on - {class}`~mqt.yaqs.core.data_structures.simulation_parameters.AnalogSimParams` - and increase `num_traj` until observables stabilize. -- Tighten `max_bond_dim` / `svd_threshold` when entanglement growth demands it. -- For trade-offs between unravellings and trajectory cost, see - {cite:p}`sander2026_computationalregimes` ({doc}`references`). +| Representation | Complex numbers in one state | For qubits | +| -------------- | ---------------------------- | ------------------------------------ | +| MPS | $O(Ld\chi^2)$. | Linear in $L$ if $\chi$ stays fixed. | +| Vector | $D$. | $2^L$. | +| Density matrix | $D^2$. | $4^L$. | -The sections below run the **same** noisy benchmark on all three paths so you -can validate agreement on small systems. We use the product state -$|{+}\rangle^{\otimes L}$ to keep the initial condition transparent. An MPS and -its exact `MPS.to_vec()` result always encode the same physical state. +Each additional qubit doubles vector storage and quadruples density-matrix +storage. Increasing an MPS bond dimension by a factor of two can increase its +state storage by about four. For strongly entangled states, the bond dimension +needed for an accurate MPS can itself grow exponentially with system size; +linear scaling at fixed bond dimension is not a guarantee for every physical +problem. See the +[TJM publication](https://www.nature.com/articles/s41467-025-66846-x) for the +trajectory formulation. -## 1. Noisy open-system benchmark +The figure counts `complex128` state arrays, at 16 bytes per entry. The MPS +curves use bond caps of 16 and 64, with each bond also limited by the dimensions +of the two subsystems it separates. These are calculated storage estimates; no +large states are allocated. -```{code-cell} ipython3 +```{code-cell} python +:tags: [hide-input] import matplotlib.pyplot as plt import numpy as np +from matplotlib_inline.backend_inline import set_matplotlib_formats -from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseModel, Observable, Simulator, State +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", + "font.serif": ["STIXGeneral"], + "mathtext.fontset": "stix", + "font.size": 11, + "axes.labelsize": 11, + "axes.linewidth": 0.7, + "xtick.labelsize": 10, + "ytick.labelsize": 10, + "xtick.direction": "in", + "ytick.direction": "in", + "xtick.top": True, + "ytick.right": True, + "legend.fontsize": 9, + "legend.frameon": False, + "lines.linewidth": 1.8, + "figure.constrained_layout.use": True, + "savefig.dpi": 180, +}) -sim = Simulator(show_progress=False) +qubits = np.arange(4, 41) +vector_bytes = 16 * 2.0**qubits +density_bytes = 16 * 4.0**qubits +mps_bytes = {} +for cap in (16, 64): + sizes = [] + for sites in qubits: + bonds = [min(cap, 2**min(cut, sites - cut)) for cut in range(sites + 1)] + sizes.append(16 * sum(2 * left * right for left, right in zip(bonds[:-1], bonds[1:], strict=True))) + mps_bytes[cap] = np.array(sizes) -L = 3 -H = Hamiltonian.ising(L, J=1.0, g=0.5) -noise = NoiseModel([{"name": "pauli_z", "sites": [i], "strength": 0.2} for i in range(L)]) -obs = Observable("x", sites=[0]) - -init_ref = State(L, initial="x+") -psi0 = init_ref.mps.to_vec() -rho0 = np.outer(psi0, psi0.conj()) -mps_tensors0 = [np.asarray(t, dtype=np.complex128).copy() for t in init_ref.mps.tensors] - -# Doc-build-friendly settings; increase t_max / num_traj for production runs. -t_max = 1.0 -dt = 0.1 -num_traj = 32 -seed = 7 - -params_rho = AnalogSimParams(observables=[obs], elapsed_time=t_max, dt=dt) -result_rho = sim.run(State(density_matrix=rho0), H, params_rho, noise) -res_rho = result_rho.expectation_values[0].flatten() -times = params_rho.times - -params_vector = AnalogSimParams( - observables=[obs], elapsed_time=t_max, dt=dt, num_traj=num_traj, random_seed=seed, -) -result_vector = sim.run(State(vector=psi0), H, params_vector, noise) -res_vector = result_vector.expectation_values[0].flatten() +fig, ax = plt.subplots(figsize=(6.6, 3.6)) +ax.semilogy(qubits, density_bytes, color="0.2", label=r"Density matrix: $4^L$") +ax.semilogy(qubits, vector_bytes, color="#D55E00", label=r"Vector: $2^L$") +ax.semilogy(qubits, mps_bytes[64], color="#0072B2", label=r"MPS: $\chi\leq64$") +ax.semilogy(qubits, mps_bytes[16], "--", color="#56B4E9", label=r"MPS: $\chi\leq16$") +ax.axhline(2.0**30, color="0.6", linewidth=0.8, linestyle=":") +ax.text(39, 2.0**30 * 1.6, "1 GiB reference", ha="right", fontsize=9, color="0.35") +ax.set(xlabel=r"Number of qubits $L$", ylabel="State storage", xlim=(4, 40), ylim=(2.0**8, 2.0**64)) +ax.set_xticks([4, 10, 20, 30, 40]) +ax.set_yticks([2.0**10, 2.0**20, 2.0**30, 2.0**40, 2.0**50, 2.0**60], + labels=["1 KiB", "1 MiB", "1 GiB", "1 TiB", "1 PiB", "1 EiB"]) +ax.legend(loc="lower left", bbox_to_anchor=(0, 1.02), ncols=2, borderaxespad=0) +ax.grid(axis="y", alpha=0.15) +plt.show() +``` + +The plot shows one state's arrays, not peak solver memory or a practical qubit +limit. The density-matrix curve continues above the plotted range. Hamiltonians, +jump operators, temporary arrays, worker copies, and saved observable +trajectories also use memory. In particular, a cached propagator can be much +larger than the state it evolves. + +## How solver work scales + +For an MPS with fixed local dimension, Hamiltonian MPO bond dimension, and local +solver effort, a TDVP sweep costs roughly $O(L\chi^3)$. The MPO bond dimension +measures the size of the Hamiltonian's tensor-network representation. More +complex interactions and growing state bonds increase the work. This estimate +describes local TDVP sweeps, not every supported integrator or noise pattern. + +Dense solvers have two regimes in the current implementation: + +| Solver | Small-system method | Method above the cache threshold | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | +| Vector / MCWF | Build a dense $D\times D$ step propagator: roughly $O(D^3)$ setup, $O(D^2)$ storage and work per step. | Apply the exponential through sparse Krylov methods; work depends on operator nonzeros and iteration count. | +| Density matrix / Lindblad | Build a dense $D^2\times D^2$ generator and step propagator: roughly $O(D^6)$ exponential setup, $O(D^4)$ storage and work per step. | Integrate the matrix master equation with adaptive RK45, without storing the full generator. | + +For a fixed number of Krylov iterations and short-range sparse operators, vector +evolution typically needs order $LD$ work per exponential action. For a +Hamiltonian with order $LD$ nonzeros and $K$ local jump channels, one Lindblad +derivative evaluation costs order $(L+K)D^2$. The number of adaptive steps +depends on the dynamics and tolerances, so these estimates do not give a single +runtime law for every problem. + +:::{dropdown} Propagator thresholds and other memory costs +The vector solver caches its dense step propagator when `D <= 4096` (up to 12 +qubits). The density-matrix solver builds a dense generator and caches its step +propagator when `D**2 <= 4096` (up to 6 qubits). At either upper threshold, one +propagator alone occupies 256 MiB in `complex128`; preprocessing needs +additional arrays. These are implementation thresholds, not recommended system +sizes or user memory limits. + +MPS evolution also stores the Hamiltonian MPO and contraction environments. For +uniform dimensions and an MPO bond dimension $w$, their typical storage is +$O(Ld^2w^2)$ and $O(Lw\chi^2)$, respectively. Intermediate bonds can exceed the +final retained bonds during an update. + +For unequal local dimensions, replace $D=d^L$ with $D=\prod_i d_i$. The exact +MPS state entry count is $\sum_i d_i\chi_i\chi_{i+1}$, where the end bonds have +dimension one. Local dimensions above two can raise costs sharply even when the +number of sites stays fixed. +::: + +### Trajectories, time steps, and parallelism -params_mps = AnalogSimParams( - observables=[obs], elapsed_time=t_max, dt=dt, num_traj=num_traj, max_bond_dim=16, random_seed=seed, +Noisy TJM and MCWF work grows approximately in proportion to `num_traj` and the +number of evolution steps, at fixed numerical settings. Standard Monte Carlo +error decreases as $1/\sqrt{N_{\mathrm{traj}}}$: halving that error generally +requires four times as many trajectories. Increasing the trajectory count does +not remove timestep or MPS approximation errors. + +Lindblad evolution needs one deterministic run, so increasing `num_traj` has no +effect. Parallel workers can reduce the wall time of trajectory ensembles, but +they do not reduce the total numerical work and can increase memory use. There +is no universal fastest representation; the balance depends on the model, +accuracy, and hardware. The +[computational-regimes study](https://arxiv.org/abs/2606.13779) compares +trajectory costs and sampling effort. + +## Compare the same noisy dynamics + +A four-site version of the XY chain in {doc}`analog_simulation` provides a small +comparison. One excitation starts at site 1, moves between neighbors, and can be +lost through local relaxation. We measure the occupation of its starting site, +$n_1=(1-Z_1)/2$, using identical physical inputs for all three solvers. + +```{code-cell} python +from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseModel, Observable, Simulator, State + +length = 4 +initial_site = 1 +basis = "0" * initial_site + "1" + "0" * (length - initial_site - 1) +hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0) +noise = NoiseModel([ + {"name": "lowering", "sites": [site], "strength": 0.6} + for site in range(length) +]) +params = AnalogSimParams( + observables=[Observable("z", initial_site)], + elapsed_time=1.0, + dt=0.05, + num_traj=64, + random_seed=7, ) -result_mps = sim.run(State(L, tensors=[t.copy() for t in mps_tensors0]), H, params_mps, noise) -res_mps = result_mps.expectation_values[0].flatten() +sim = Simulator(show_progress=False) ``` -```{code-cell} ipython3 ---- -mystnb: - image: - width: 80% - align: center ---- -fig, ax = plt.subplots(figsize=(6, 3.5), layout="constrained") -ax.plot(times, res_rho, label="density_matrix (exact)", linewidth=2, color="black") -ax.plot(times, res_vector, label=f"vector ({num_traj} traj)", linestyle="--") -ax.plot(times, res_mps, label=f"mps ({num_traj} traj)", linestyle=":") -ax.set_xlabel("Time") -ax.set_ylabel(r"$\langle X_0 \rangle$") -ax.set_ylim(-1.05, 1.05) -ax.legend() -ax.set_title(r"Open-system evolution across representations ($|+\rangle^{\otimes L}$ init)") -ax.grid(alpha=0.3) -plt.show() -``` +Only the `State` representation changes between runs. Product-state presets +create the same initial physical state directly in each representation, so no +manual tensor copying or dense conversion is needed: + +```{code-cell} python +results = {} +for representation in ("density_matrix", "vector", "mps"): + state = State(length, initial="basis", basis_string=basis, representation=representation) + results[representation] = sim.run(state, hamiltonian, params, noise_model=noise) -```{note} -`vector` and `mps` curves are Monte Carlo means over `num_traj` trajectories; -statistical error scales as $1/\sqrt{N_{\mathrm{traj}}}$. The `density_matrix` -path returns the deterministic ensemble average directly. Increase `num_traj` -and `t_max` for smoother curves. +times = results["density_matrix"].times ``` -## 2. Noiseless cross-check +Parallel execution remains enabled, and the documentation hides progress bars. +Run the cells in order in a notebook; see {doc}`simulator_initialization` for +the main guard required in a script. The MPS and vector results each store an +array of means in `expectation_values[0]` and an array of shape `(64, 21)` in +`trajectories[0]`. The density-matrix result has a single deterministic row. -With `noise_model=None`, all three representations should agree on unitary -observables (single trajectory for `mps` and `vector`). We use a product state -here so the MPS path is exact at modest bond dimension. +```{code-cell} python +:tags: [hide-input] +fig, ax = plt.subplots(figsize=(6.6, 3.2)) +reference = (1 - results["density_matrix"].expectation_values[0]) / 2 +ax.plot(times, reference, color="0.2", label="Lindblad reference") +for representation, color, marker, style, offset, label in ( + ("mps", "#0072B2", "o", "-", 0, "TJM / MPS"), + ("vector", "#D55E00", "s", "--", 1, "MCWF / vector"), +): + result = results[representation] + mean = (1 - result.expectation_values[0]) / 2 + samples = (1 - result.trajectories[0]) / 2 + standard_error = samples.std(axis=0, ddof=1) / np.sqrt(samples.shape[0]) + ax.fill_between(times, mean - standard_error, mean + standard_error, color=color, alpha=0.14, linewidth=0) + ax.plot(times, mean, color=color, marker=marker, linestyle=style, + markevery=(offset, 2), markersize=3.2, markerfacecolor="white", + linewidth=1.2, label=label) +ax.set(xlabel=r"Time $t$", ylabel=r"Occupation $\langle n_1\rangle$", xlim=(0, 1), ylim=(-0.03, 1.04)) +ax.legend(loc="upper right") +plt.show() +``` -```{code-cell} ipython3 -obs_z = Observable("z", sites=[0]) -params_mps_u = AnalogSimParams(observables=[obs_z], elapsed_time=0.5, dt=0.1, max_bond_dim=16) -params_rho_u = AnalogSimParams(observables=[obs_z], elapsed_time=0.5, dt=0.1) +The lines show excitation transport combined with loss. Shaded bands give one +estimated standard error of the trajectory means at each time; they are not +bounds on the numerical error or simultaneous confidence bands. Sixty-four +trajectories illustrate statistical variation, not a converged benchmark or a +runtime comparison. The shared seed can correlate the two trajectory estimates. +Lindblad supplies a deterministic numerical reference, not an error-free +solution. -init_product = State(L, initial="x+") -psi_prod = init_product.mps.to_vec() -rho_prod = np.outer(psi_prod, psi_prod.conj()) -mps_prod = [np.asarray(t, dtype=np.complex128).copy() for t in init_product.mps.tensors] +## Check accuracy and supported workflows -z_mps = sim.run(State(L, tensors=mps_prod), H, params_mps_u, None).expectation_values[0][-1] -z_vec = sim.run(State(vector=psi_prod), H, params_mps_u, None).expectation_values[0][-1] -z_rho = sim.run(State(density_matrix=rho_prod), H, params_rho_u, None).expectation_values[0][-1] -``` +For noisy MPS and vector results, increase `num_traj` to test sampling error, +then reduce `dt` to check time resolution. For MPS, also increase `max_bond_dim` +and reduce `svd_threshold` to check compression. A product initial state can +become entangled during evolution; its initial simplicity does not make the +later MPS approximation exact. Use {doc}`simulation_parameters` for presets and +solver-specific controls. + +| Workflow or input | Supported representations | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Static-Hamiltonian analog evolution | MPS, vector, and density matrix; noise must meet the restrictions in {doc}`realistic_noise_models`. | +| Circuits and analog-digital programs | MPS. | +| Mixed initial state | Density matrix. | +| Entropy and Schmidt-spectrum observables | MPS; noisy results describe pure trajectories, not the spectrum of a mixed density matrix. | +| Piecewise Hamiltonian | MPS with TDVP; see {doc}`hamiltonians`. | +| Unitary `list[State]` ensemble | MPS; see {doc}`ensemble_evolution`. | -## Related topics +Noisy MPS, vector, and circuit runs do not return one final pure state for the +trajectory ensemble. Noisy density-matrix evolution can retain its final mixed +state with `get_state=True`. -- {doc}`analog_simulation` — TJM workflow with MPS -- {doc}`state_initialization` — choosing a representation -- {doc}`simulation_parameters` — presets, `num_traj`, and truncation -- {doc}`quickstart` — minimal first simulation +Choose MPS when the required bond dimensions remain affordable, vector when a +full pure state fits and provides a useful reference, and density matrix when +you need a small-system ensemble average or mixed-state evolution. Check +convergence for the quantities you intend to report before scaling up. diff --git a/docs/examples/scheduled_jumps.md b/docs/examples/scheduled_jumps.md index e5c58a283..c6988b375 100644 --- a/docs/examples/scheduled_jumps.md +++ b/docs/examples/scheduled_jumps.md @@ -1,178 +1,8 @@ --- -file_format: mystnb -kernelspec: - name: python3 -mystnb: - number_source_lines: true - execution_timeout: 300 +orphan: true --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Scheduled Jumps -This example demonstrates how to use **scheduled noise jumps** in YAQS. -Scheduled jumps allow you to apply specific operators at predetermined times -during an analog simulation. This is useful for simulating controlled gates, -sudden noise events, or time-dependent perturbations without needing a full -time-dependent Hamiltonian. - -In this example, we simulate a 10-site Ising chain and apply a scheduled Pauli-X -flip to a specific site at $t=1.0$. - -```{important} -Scheduled jumps are supported only for **single-`State` analog MPS TJM** with -{class}`~mqt.yaqs.AnalogSimParams` ``order=1`` (the default). They are rejected -for ``order=2``, MCWF, Lindblad, digital circuits, and `list[State]` unitary -ensembles. Jump times must lie on the simulation time grid (`sim_params.times`; -choose `time` as a multiple of `dt`). Two-site scheduled jumps must be -nearest-neighbor. A jump at ``time=0.0`` is applied to the initial state -**before** dissipation and the first measurement, so the recorded initial -observable (and ``get_state``) already include it. For interior timesteps, on a -matching grid point scheduled jumps replace the ordinary stochastic jump channel -for that step (dissipation still runs). -``` - -## 1. Setup - -First, we define the Hamiltonian and the initial state. We'll use a standard -transverse-field Ising model. - -```{code-cell} ipython3 -from mqt.yaqs import Hamiltonian, State - -L = 10 -J = 1.0 -g = 1.0 - -# Hamiltonian: H = -J Σ Z_i Z_{i+1} - g Σ X_i -hamiltonian = Hamiltonian.ising(length=L, J=J, g=g) - -# Initial state: all zeros |00...0> -state = State(L, initial="zeros") -``` - -## 2. Define the Scheduled Jump - -We define a scheduled jump using a list of dictionaries in the `NoiseModel`. -Each dictionary must specify: - -- `time`: The time at which to apply the jump. -- `sites`: A list of site indices the jump acts on. -- `name`: The name of the jump operator (e.g., `"x"`, `"y"`, `"z"`, - `"crosstalk_xx"`), **or** any label when you pass a custom `matrix` (see - below). - -If `matrix` is omitted, `name` is resolved from -{class}`~mqt.yaqs.core.libraries.noise_library.NoiseLibrary`. To apply a custom -operator, add a `matrix` key with a local `d×d` NumPy array (`d=2` for qubits); -`name` is then only an identifier. See {doc}`realistic_noise_models` § 6 for the -full process-dict schema. - -```{code-cell} ipython3 -from mqt.yaqs import NoiseModel - -jump_time = 1.0 -jump_site = 5 # Apply jump to the middle site - -# Schedule a Pauli-X flip on site 5 at t=1.0 -scheduled_jumps = [{"time": jump_time, "sites": [jump_site], "name": "x"}] -noise_model = NoiseModel(scheduled_jumps=scheduled_jumps) -``` - -### Custom operator example - -A $\pi/2 rotation about $Y$ can be scheduled explicitly instead of using a -library name: - -```{code-cell} ipython3 -import numpy as np - -ry_pi2 = np.array([[1, -1], [1, 1]], dtype=complex) / np.sqrt(2) - -custom_jump = [{"time": jump_time, "sites": [jump_site], "name": "ry_pi2", "matrix": ry_pi2}] -custom_noise_model = NoiseModel(scheduled_jumps=custom_jump) -``` - -## 3. Simulation Parameters - -We measure the $Z$ expectation value on the **jumped site** -($\langle Z_5 \rangle$). A Pauli-X jump flips that site's magnetization; -measuring a distant site would show only a weak entanglement signal and can look -like no jump occurred. - -```{code-cell} ipython3 -from mqt.yaqs import AnalogSimParams, Observable - -z_obs = Observable("z", sites=jump_site) - -sim_params = AnalogSimParams( - elapsed_time=5.0, - dt=0.1, - num_traj=1, # Jumps are deterministic, so 1 trajectory is sufficient - observables=[z_obs], -) -``` - -## 4. Run Simulation - -We run two simulations: one with the jump and a baseline without it. - -```{code-cell} ipython3 -from mqt.yaqs import Simulator -import copy - -sim = Simulator(show_progress=False) - -# Baseline -state_baseline = copy.deepcopy(state) -sim_params_baseline = copy.deepcopy(sim_params) -result_baseline = sim.run(state_baseline, hamiltonian, sim_params_baseline) - -# With Jump -state_jump = copy.deepcopy(state) -sim_params_jump = copy.deepcopy(sim_params) -result_jump = sim.run(state_jump, hamiltonian, sim_params_jump, noise_model=noise_model) -``` - -## 5. Visualize Results - -We plot the expectation value $\langle Z_{\text{jump site}} \rangle$ over time. - -```{code-cell} ipython3 ---- -mystnb: - image: - width: 80% - align: center ---- -import matplotlib.pyplot as plt - -times = sim_params_jump.times -res_baseline = result_baseline.expectation_values[0] -res_jump = result_jump.expectation_values[0] - -plt.figure(figsize=(8, 5)) -plt.plot(times, res_baseline, label="Baseline (No Jump)", color="black", linestyle="--") -plt.plot(times, res_jump, label=f"Jump on site {jump_site}", color="tab:blue") -plt.axvline(x=jump_time, color='red', linestyle=':', label="Jump Time") - -plt.xlabel("Time (t)") -plt.ylabel(f"$\\langle Z_{{{jump_site}}} \\rangle$") -plt.title(f"Effect of a Scheduled Jump at $t={jump_time}$ on site {jump_site}") -plt.legend() -plt.grid(True, alpha=0.3) -plt.show() -``` - -## Related topics - -- {doc}`analog_simulation` — TJM workflow and noise models -- {doc}`digital_analog_simulation` — scheduled jumps inside a mixed program - (times are local to each analog run) -- {doc}`realistic_noise_models` — built-in and custom jump operators, - distributed strengths -- {doc}`simulation_parameters` — time grids and `dt` alignment +See {ref}`noise-scheduled-jumps` in {doc}`realistic_noise_models` for fixed-time +operators, custom matrices, and the supported backends and time grids. diff --git a/docs/examples/simulation_parameters.md b/docs/examples/simulation_parameters.md index 375d730d1..d97998cf4 100644 --- a/docs/examples/simulation_parameters.md +++ b/docs/examples/simulation_parameters.md @@ -2,6 +2,8 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true execution_timeout: 300 @@ -9,355 +11,262 @@ mystnb: # Configuring Simulation Parameters -YAQS separates **what you evolve** ({class}`~mqt.yaqs.State`, circuits, -Hamiltonians) from **how you truncate and sample** via parameter objects passed -to {meth}`~mqt.yaqs.Simulator.run`: - -| Class | Use when | -| ----------------------------------- | -------------------------------------------------------------------------------------------- | -| {class}`~mqt.yaqs.AnalogSimParams` | Open-system or unitary time evolution (TDVP / BUG, MCWF trajectories, Lindblad-style paths). | -| {class}`~mqt.yaqs.DigitalSimParams` | Circuit simulation: observables, computational-basis shots, or both. | - -This page shows how to construct each class. For {class}`~mqt.yaqs.Simulator` -execution options (parallelism, progress bars), see -{doc}`simulator_initialization`. - -## Observable string names - -{class}`~mqt.yaqs.Observable` accepts a named Hermitian operator as its first -argument. YAQS resolves the name internally, so standard measurements do not -depend on gate classes. - -| String | Meaning | Example | -| ------------------------ | -------------------------------------------------------------- | ------------------------------------------- | -| `"x"`, `"y"`, `"z"` | Single-qubit Pauli operators | `Observable("z", sites=0)` | -| `"id"` | Single-site identity operator | `Observable("id", sites=0)` | -| `"p0"`, `"p1"` | Single-site computational-basis projectors | `Observable("p0", sites=0)` | -| `"xx"`, `"yy"`, `"zz"` | Two-qubit Pauli strings | `Observable("zz", sites=[0, 1])` | -| `"position"` | Position operator for a supplied local position basis | `Observable("position", 0, positions=grid)` | -| `"entropy"` | Bipartite entanglement entropy across a cut | `Observable("entropy", sites=cut)` | -| `"schmidt_spectrum"` | Schmidt spectrum across a cut | `Observable("schmidt_spectrum", sites=cut)` | -| binary bitstring | Projection-valued measurement onto a computational basis state | see {doc}`circuit_observables` | - -Observable matrices must be Hermitian. For custom unitaries and circuit gates, -use {doc}`custom_gates`; those workflows use `GateLibrary` or Qiskit circuits -directly. - -Named observables that require configuration accept keyword-only factory -arguments. Missing or unknown arguments raise `TypeError`, so misspelled -parameters are not silently ignored. - -## Start with a preset - -You do **not** need to tune every numerical knob before running a simulation. -Pick a **preset** and let it fill in the truncation and sampling settings you -may be unfamiliar with (`svd_threshold`, `max_bond_dim`, `num_traj` on -analog/digital-observable runs, and `krylov_tol`). - -All `*SimParams` classes accept a keyword-only `preset` argument (default -`"balanced"`): - -| `preset` | `svd_threshold` | `max_bond_dim` | `num_traj` (analog / digital observables) | `krylov_tol` | -| ---------------------- | --------------- | -------------- | ----------------------------------------- | ------------ | -| `"fast"` | `1e-3` | `16` | `128` | `1e-3` | -| `"balanced"` (default) | `1e-6` | `128` | `256` | `1e-4` | -| `"accurate"` | `1e-9` | `4096` | `1024` | `1e-6` | -| `"exact"` | `1e-13` | `None` | `1024` | `1e-12` | - -- **`"fast"`** — qualitative exploration and quick tests; not intended for - strict dense comparisons. -- **`"balanced"`** — recommended default for exploratory work. -- **`"accurate"`** — high-quality production settings. -- **`"exact"`** — strict reference/debug preset with minimal internal numerical - relaxation. Stochastic trajectory sampling, finite time steps, and model error - still apply; this is not mathematically exact. - -`svd_threshold` controls **tensor-network SVD truncation** (bond truncation). -`krylov_tol` controls the **adaptive Krylov/Lanczos matrix exponential** inside -TDVP updates. These are independent: tightening one does not change the other. -`trunc_mode` (default `"discarded_weight"`) is unchanged across presets. The -chosen preset name is stored on the object as `params.preset`. - -For analog BUG evolution, set `evolution_mode=EvolutionMode.BUG` (exported from -`mqt.yaqs`). BUG uses center-augmented alternating endpoints with one -compression and renormalization after each `dt` step; see -{doc}`analog_simulation`. - -## Override only what you need - -**Explicit constructor arguments override the preset; everything you omit keeps -the preset value.** - -That is the intended workflow when you know _some_ settings but not all: - -1. Choose the closest preset (`"fast"`, `"balanced"`, `"accurate"`, or - `"exact"`). -2. Pass **only** the fields you want to change. -3. Leave the rest unset — they stay at the preset defaults. - -Overridable preset fields: - -| Argument | What it controls | -| --------------- | -------------------------------------------------- | -| `svd_threshold` | SVD bond truncation during MPS/MPO updates | -| `max_bond_dim` | Hard cap on bond dimension (`None` = no cap) | -| `num_traj` | Trajectory count (analog / digital observables) | -| `krylov_tol` | Adaptive Krylov/Lanczos matrix exponential in TDVP | - -Optional `shots` on `DigitalSimParams` is set explicitly and is **not** part of -any preset. - -If you omit an overridable argument, the preset supplies it. If you pass a value -explicitly, **that value wins** for that field only — the other preset fields -are unchanged. For `max_bond_dim`, omit the argument to keep the preset cap; -pass `None` explicitly to remove the cap. - -## Recommended usage - -```{code-cell} ipython3 -from mqt.yaqs import ( - SIMULATION_PRESETS, - AnalogSimParams, - DigitalSimParams, - Observable, -) +Simulation parameters choose what to measure, when to record it, and the +numerical accuracy. Start with a preset, then change the settings that matter +for your calculation. +| Class | Use for | +| ------------------ | ---------------------------------------------------------------------- | +| `AnalogSimParams` | Hamiltonian evolution, including noisy dynamics and unitary ensembles. | +| `DigitalSimParams` | Circuit expectation values, shot counts, or a final state. | -def _trunc_summary(params: AnalogSimParams | DigitalSimParams) -> dict[str, object]: - """Collect preset-related fields for display.""" - out: dict[str, object] = { - "preset": params.preset, - "svd_threshold": params.svd_threshold, - "max_bond_dim": params.max_bond_dim, - "krylov_tol": params.krylov_tol, - } - if isinstance(params, DigitalSimParams) and params.shots is not None: - out["shots"] = params.shots - out["num_traj"] = params.num_traj - return out -``` +Pass the parameter object to `Simulator.run` with the initial state and +Hamiltonian or circuit. Supply noise through `noise_model` in that call. The +state representation selects the analog backend; see +{doc}`representation_comparison`. Parallel execution and progress controls +belong to `Simulator`, as described in {doc}`simulator_initialization`. -Pick a preset — no other truncation arguments required: +## Choose a preset -```{code-cell} ipython3 -# Default: balanced preset fills in all truncation settings -analog_params = AnalogSimParams() +Both classes default to `preset="balanced"`. A preset supplies four settings: -for name in ("fast", "balanced", "accurate", "exact"): - _trunc_summary(AnalogSimParams(preset=name)) -``` +| Preset | `svd_threshold` | `max_bond_dim` | `num_traj` | `krylov_tol` | +| ------------ | --------------- | -------------- | ---------- | ------------ | +| `"fast"` | `1e-3` | `16` | `128` | `1e-3` | +| `"balanced"` | `1e-6` | `128` | `256` | `1e-4` | +| `"accurate"` | `1e-9` | `4096` | `1024` | `1e-6` | +| `"exact"` | `1e-13` | `None` | `1024` | `1e-12` | -Override **one** field; the rest stay from `"balanced"`: +Use `"fast"` to explore a model and `"balanced"` as a starting point for +accuracy checks. `"accurate"` tightens the numerical settings and increases the +trajectory budget, at greater cost. `"exact"` provides strict reference settings +and removes the bond cap; finite timesteps, truncation, and sampling error still +apply. No preset establishes convergence for every model. -```{code-cell} ipython3 -balanced = AnalogSimParams(preset="balanced") -tighter_krylov = AnalogSimParams(preset="balanced", krylov_tol=1e-8) -``` +Explicit constructor arguments replace only the corresponding preset values: -Override **several** fields when you know exactly what you want; the remaining -preset fields still apply: +```{code-cell} python +from mqt.yaqs import AnalogSimParams, DigitalSimParams, Observable -```{code-cell} ipython3 custom_params = AnalogSimParams( - preset="fast", # start from fast defaults for everything else - max_bond_dim=512, - num_traj=32, + preset="accurate", + max_bond_dim=256, + num_traj=64, ) -_trunc_summary(custom_params) ``` -Shot-based circuit simulation: set `shots` yourself, use a preset for -truncation: +Here, `svd_threshold` and `krylov_tol` retain their `"accurate"` values. Omit +`max_bond_dim` to keep the preset cap; pass `None` explicitly to remove it. +Choose the preset when constructing the object. Assigning a new value to +`params.preset` later does not reset the other fields. `shots` is always an +explicit budget and is not part of a preset. -```{code-cell} ipython3 -shot_params = DigitalSimParams( - shots=1024, - preset="fast", +## Set analog measurements and times + +For a four-site chain, record the local Pauli $Z$ expectations over two time +units: + +```{code-cell} python +analog_params = AnalogSimParams( + observables=[Observable("z", site) for site in range(4)], + elapsed_time=2.0, + dt=0.05, ) -_trunc_summary(shot_params) ``` -## `AnalogSimParams` +The default `sample_timesteps=True` records 41 samples, including time zero and +the final time. After the run, `result.times` holds the sampled times and +`result.expectation_values[i]` holds the values for the $i$th supplied +observable. Set `sample_timesteps=False` to keep only the final sample; +evolution still uses the same `dt`. -Besides the preset (and any overrides), you typically set the time grid -(`elapsed_time`, `dt`), observables, and whether to record intermediate times -(`sample_timesteps`). Analog backends use a fixed step size, so `dt` must be -positive and `elapsed_time` must be a non-negative integer multiple of `dt`. -Non-integral grids raise a `ValueError`; choose the step count first and set -`elapsed_time = num_steps * dt` when constructing a grid programmatically. +The timestep must be positive, and the duration must be non-negative and an +integer multiple of `dt`. For a computed grid, set +`elapsed_time = num_steps * dt` to keep the duration consistent with the step +count. Invalid grids raise `ValueError`. -```{code-cell} ipython3 -L = 4 -observables = [Observable("z", site) for site in range(L)] +For noisy trajectory evolution, `num_traj` sets the number of realizations to +average. A noiseless single-state run uses one realization. A density-matrix +backend evolves the ensemble directly, while a supplied `list[State]` uses the +list length as its ensemble size. See {doc}`analog_simulation` for a noisy +walkthrough and {doc}`ensemble_evolution` for unitary ensemble averages. -analog = AnalogSimParams( - observables=observables, - elapsed_time=0.2, - dt=0.05, - preset="accurate", +## Choose circuit outputs + +Request observables, shots, or both. At least one output must be requested in a +standalone run; `get_state=True` is another option for supported noiseless runs. + +```{code-cell} python +observable_params = DigitalSimParams(observables=[Observable("z", 0)]) +shot_params = DigitalSimParams(shots=1024) +combined_params = DigitalSimParams( + observables=[Observable("z", 0)], + shots=1024, + num_traj=64, ) -_trunc_summary(analog) ``` -Need a smaller bond cap for a quick test, but keep the rest of `"accurate"`? - -```{code-cell} ipython3 -analog_quick = AnalogSimParams( - observables=observables, - elapsed_time=0.2, +Observables are recorded at the circuit's end by default. With +`sample_layers=True`, YAQS also records the initial state and checkpoints marked +by `circuit.barrier(label="SAMPLE_OBSERVABLES")`. It does not sample every +circuit layer automatically. See {doc}`circuit_observables` for checkpoint +placement and the resulting sample axis. + +`shots` is the total readout budget. `num_traj` controls the noisy observable +ensemble, so the two settings have different roles: + +| Run | How YAQS uses the budgets | +| ----------------------- | ------------------------------------------------------------------------------- | +| Noiseless | Evolve once and sample all requested shots from the final state. | +| Noisy, with observables | Average `num_traj` trajectories. Distribute any requested shots across them. | +| Noisy, shots only | Run one single-shot trajectory per shot; `num_traj` does not control this path. | + +A combined run supports `shots < num_traj`: every trajectory contributes to the +observable mean, while some receive no readout samples. Counts appear in +`result.counts` as integer keys and integer counts. Site 0 is the +least-significant bit, matching Qiskit's `int(bitstring, 2)` convention. See +{doc}`circuit_shots` for histogram plotting. + +## Choose observables and diagnostics + +Named operators avoid imports from gate libraries. The Pauli names below refer +to $\sigma^\alpha$, rather than spin operators $S^\alpha=\sigma^\alpha/2$. + +| Request | Example | +| --------------------------------- | ---------------------------------------------------------------------- | +| Single-site Pauli operator | `Observable("z", 0)`; also `"x"` and `"y"` | +| Identity or basis projector | `Observable("id", 0)`, `Observable("p0", 0)`, or `Observable("p1", 0)` | +| Two-site Pauli operator | `Observable("zz", [0, 1])`; also `"xx"` and `"yy"` | +| Position on a supplied grid | `Observable("position", 0, positions=grid)` | +| Probability of a full basis state | `Observable("0101")` for a four-qubit system | +| Custom Hermitian operator | `Observable(matrix, sites=0)` or `Observable(matrix, sites=[0, 1])` | + +Two-site local operators support adjacent sites and the periodic end-to-end bond +on qubit chains. Matrix factors follow the supplied site order. Bitstring +probability requests cannot share an observable list with ordinary operators or +entanglement diagnostics; shot counts can accompany ordinary observables. + +For MPS entanglement across the bond between sites 1 and 2, request the adjacent +pair: + +```{code-cell} python +diagnostic_params = AnalogSimParams( + observables=[ + Observable("entropy", sites=[1, 2]), + Observable("schmidt_spectrum", sites=[1, 2]), + ], + elapsed_time=2.0, dt=0.05, - preset="accurate", - max_bond_dim=256, ) -_trunc_summary(analog_quick) ``` -Pass the resulting object to {meth}`~mqt.yaqs.Simulator.run` together with a -{class}`~mqt.yaqs.State` and {class}`~mqt.yaqs.Hamiltonian` (see -{doc}`analog_simulation`). - -## `DigitalSimParams` - -Used for circuit simulation. Set non-empty `observables` for expectation values, -`shots` for computational-basis counts, and/or `get_state` for the final MPS. -Observables and shots may be requested together. Optionally enable layer -sampling with `sample_layers=True` (see {doc}`circuit_observables`). - -`num_traj` and `shots` are **independent** controls: - -| Parameter | Meaning | -| ---------- | ------------------------------------------------------------------------ | -| `num_traj` | Noisy stochastic trajectories for observables and trajectory diagnostics | -| `shots` | Total bitstring-sample budget | - -- **Noisy + observables (+ optional shots):** run `num_traj` trajectories. If - `shots` is also set, that **total** budget is distributed across those - trajectories. `shots < num_traj` is supported: some trajectories still - contribute observables but receive zero measurement samples. -- **Noisy + shots only:** `num_traj` is ignored; one single-shot trajectory is - run per shot, so configuring `num_traj` does not affect this path. -- **Noiseless:** one trajectory is enough; all `shots` are sampled from that - final state. - -### Two-qubit gate mode (`gate_mode`) - -Digital circuit simulation on an MPS defaults to **`gate_mode="mpo"`** (generic -MPO--MPS application): nearest-neighbor gates use the same local TEBD/SVD path -as `swaps` (the orthogonality center is moved onto the gate pair first, so the -truncated SVD discards the smallest Schmidt coefficients of the state), and -long-range gates contract an extended gate MPO site-wise (library leg ordering, -MPS virtual index before MPO virtual index) followed by compression with -`svd_threshold` and `max_bond_dim`. Other modes differ only in how two-qubit -gates are applied: - -- **`swaps`** — TEBD/SVD for every two-qubit gate; long-range gates are routed - with adjacent SWAP insertion before and after the local update. -- **`tdvp`** — TEBD/SVD on nearest-neighbor gates; long-range gates use the - generator MPO + **two-site TDVP (2TDVP)** on a local window. -- **`full-tdvp`** — TDVP (generator MPO + 2TDVP on a local window) on every - two-qubit gate. - -Matrix-backed custom gates (from Qiskit `UnitaryGate` or other unknown -unitaries) have no analytic generator. In `gate_mode="tdvp"` or `"full-tdvp"`, -those gates use TEBD on nearest-neighbor pairs and the MPO path on long-range -pairs instead of the TDVP generator window. See {doc}`custom_gates` for the full -gate translation and custom-gate workflow. - -Gates on three or more qubits have no TEBD path: in the TDVP modes, gates with a -product-form generator (`ccx`, `ccz`) use the generator MPO and TDVP window; all -other cases, including `gate_mode="swaps"`, use the extended gate MPO. - -Long-range gates in `gate_mode="tdvp"` apply 2TDVP on the gate support window -via `evolve_window`. - -```{warning} -`"tdvp"` and `"full-tdvp"` are variational, approximate gate paths. With one -sweep, a long-range entangling gate can fail to create every required Schmidt -rank. This affects a long-range CZ on a product MPS, rank growth from two to -four after a shallow preparation, and multi-gate RZZ ladders. More sweeps can -improve some cases but do not guarantee exact gate application. Use the default -`"mpo"` mode, or `"swaps"` for two-qubit gates, when gate-application accuracy -up to the configured truncation is required. -``` +These diagnostics work at sampled times or circuit checkpoints, including noisy +MPS runs. Spectra contain descending Schmidt coefficients, with up to 500 +entries and `NaN` padding. With noise, YAQS averages the pure-trajectory +entropies and coefficients; these are not the entropy or spectrum of the +ensemble's mixed density matrix. Missing ranks contribute zero to coefficient +means, while ranks absent from every trajectory remain `NaN`. + +## Refine accuracy and retain results -Use **`tdvp_sweeps`** (default `1`) to split each TDVP evolution step into -multiple substeps of equal total time. Values greater than `1` are opt-in and -may improve accuracy on some circuits. The setting applies to all TDVP kernels -on `AnalogSimParams` and `DigitalSimParams`. +Change one source of error at a time and compare the observable that matters for +your calculation: -Use **`tdvp_mode`** to select the TDVP integrator: `"1site"` (1TDVP), `"2site"` -(2TDVP), or `"dynamic"` (adaptive single/two-site updates). The default is -**`"2site"`** (2TDVP) on `AnalogSimParams` and `DigitalSimParams`. Pass -`"dynamic"` explicitly for adaptive 1/2-site switching during analog evolution. +| Setting | When to change it | +| --------------- | ---------------------------------------------------------------------------------------------------------- | +| `dt` | Reduce it at fixed analog duration to check time-step error. | +| `num_traj` | Increase it to reduce uncertainty in noisy trajectory averages. | +| `max_bond_dim` | Increase the MPS bond cap if it limits the evolving state. Larger bonds cost memory and time. | +| `svd_threshold` | Reduce it to retain more information during tensor truncation. | +| `krylov_tol` | Reduce it to tighten local matrix-exponential solves in TDVP or BUG. This does not tighten SVD truncation. | -Substep geometry: each substep is **symmetric** (left-to-right then -right-to-left) at evolution time `step_time / tdvp_sweeps` for analog (`dt`) and -digital gates. The total generator time applied to one digital gate remains `1` -across all substeps. Noise and dissipation after TDVP still use the full -physical step `dt` in analog simulation. +Keep an explicit trajectory budget when comparing presets, since a preset +changes that budget along with the numerical settings. Tensor truncation and +integrator controls concern MPS evolution; dense backends use different +numerical methods. See {doc}`representation_comparison` before changing them. -```{code-cell} ipython3 -digital = DigitalSimParams( - observables=[Observable("z", 0)], - gate_mode="tdvp", - tdvp_sweeps=2, - preset="accurate", -) -_trunc_summary(digital) -``` +Set `random_seed` to a non-negative integer to repeat jump decisions and sampled +static disorder for the same input and configuration. It does not seed random +state initialization or measurement-shot sampling. -```{code-cell} ipython3 -digital_default = DigitalSimParams( - observables=[Observable("z", 0)], - preset="accurate", -) -_trunc_summary(digital_default) -``` +Set `get_state=True` to retain a supported final state in `result.output_state`. +Noisy MPS, statevector, and circuit runs cannot return one final pure state for +the trajectory ensemble. Noisy density-matrix evolution can return its final +mixed state. Unitary list-of-state ensembles do not return a final ensemble +state. -When `shots` is set, YAQS stores measurement histograms in `Result.counts` as a -`dict[int, int]`. The integer key encodes the measured bitstring with -**site 0 as the least-significant bit** (little-endian). This matches Qiskit’s -default convention if you interpret Qiskit bitstrings (`c_{n-1}...c_0`) via -`int(bitstring, 2)`. +## Advanced numerical options -```{code-cell} ipython3 -shot_params = DigitalSimParams(shots=1000) -shot_exact = DigitalSimParams(shots=1000, preset="exact") -``` +:::{dropdown} Analog integrators and two-time correlations -### Combined observables and shots +MPS analog evolution defaults to `evolution_mode=EvolutionMode.TDVP`. Import +`EvolutionMode` from `mqt.yaqs` and select `EvolutionMode.BUG` to use the BUG +integrator. See {doc}`analog_simulation` for the workflow. -Request both outputs on one `DigitalSimParams`. For a noisy run, set `num_traj` -for the observable ensemble and `shots` for the total sample budget: +`order=1` or `order=2` selects the TJM splitting order for noisy MPS evolution. +This is separate from `tdvp_mode`, which controls the TDVP state updates: -```{code-cell} ipython3 -combined = DigitalSimParams( - observables=[Observable("z", 0)], - shots=1000, - num_traj=64, -) -# Example of shots < num_traj (valid): two samples total, four trajectories. -combined_sparse = DigitalSimParams( - observables=[Observable("z", 0)], - shots=2, - num_traj=4, -) -_trunc_summary(combined) -``` +- `"2site"` (default) allows bond growth through two-site updates. +- `"1site"` uses single-site updates with fixed bond dimensions. +- `"dynamic"` switches between single-site and two-site updates. -See {doc}`circuit_shots` for a full shot-readout example. +`tdvp_sweeps` defaults to 1. Increasing it subdivides each TDVP evolution step +into symmetric substeps with the same total evolution time. In noisy analog +runs, noise still acts on the full physical timestep `dt`; extra TDVP substeps +do not refine that noise timestep. -## Reference: preset table in code +For two-time correlations, pass `multi_time_observables=[(A, B)]` with a +noiseless MPS `list[State]` and a static Hamiltonian. `B` acts at time zero and +`A` at the later time. The complex results appear in `multi_time_results`, with +one row per pair. See {doc}`ensemble_evolution` for the definition and example. -The built-in values are defined in {data}`~mqt.yaqs.SIMULATION_PRESETS`: +::: -```{code-cell} ipython3 -SIMULATION_PRESETS -``` +:::{dropdown} Circuit gate-application modes + +Keep `gate_mode="mpo"` unless you need to compare another method. It applies +gates directly and compresses the result using the configured tensor truncation. +The available modes are: + +| Mode | Two-qubit gates | +| ----------------- | ----------------------------------------------------------------------------------- | +| `"mpo"` (default) | Direct local updates for neighbors; an extended gate MPO for separated sites. | +| `"swaps"` | Route separated sites together with SWAPs, apply the gate, and restore their order. | +| `"tdvp"` | Direct local updates for neighbors; generator-based TDVP for separated sites. | +| `"full-tdvp"` | Generator-based TDVP for both neighboring and separated sites. | + +TDVP gate paths are variational approximations and can miss the bond growth an +entangling gate requires. Additional `tdvp_sweeps` may help, but do not +guarantee exact gate application. Use `"mpo"`, or `"swaps"` for two-qubit gates, +for direct application up to the configured truncation. Digital generator-based +TDVP requires `tdvp_mode="2site"`. + +Matrix-backed custom gates without an analytic generator use direct local +updates for neighbors and the MPO path for separated sites, including in TDVP +modes. Gates on three or more qubits use an MPO, except supported product-form +generators such as `ccx` and `ccz` in TDVP modes. See +{ref}`circuit-custom-gates` for custom gate inputs. + +::: + +:::{dropdown} SVD truncation modes + +`trunc_mode="discarded_weight"` is the default. `svd_threshold` limits the sum +of discarded squared singular values. The other modes interpret the threshold as +a fraction of total squared weight (`"relative_discarded_weight"`), a ratio to +the largest singular value (`"relative"`), or an absolute singular-value cutoff +(`"hard_cutoff"`). `max_bond_dim` can force further truncation in every mode. +Presets do not change `trunc_mode`. + +::: -## Related topics +## Related guides + +- {doc}`quickstart` — simulation and characterization workflows. +- {doc}`analog_simulation` — noisy spin dynamics and convergence checks. +- {doc}`circuit_observables` — circuit dynamics and sampling checkpoints. +- {doc}`circuit_shots` — noisy readout distributions. +- {doc}`simulator_initialization` — execution controls and result fields. -- {doc}`quickstart` — minimal first simulation -- {doc}`analog_simulation` — analog parameters in context -- {doc}`circuit_observables` — observables, `gate_mode`, and layer sampling -- {doc}`circuit_shots` — shot readout with `DigitalSimParams` +Full constructor signatures are in the API reference for +{class}`~mqt.yaqs.AnalogSimParams` and {class}`~mqt.yaqs.DigitalSimParams`. diff --git a/docs/examples/simulator_initialization.md b/docs/examples/simulator_initialization.md index d8a5f89c6..d65b3697a 100644 --- a/docs/examples/simulator_initialization.md +++ b/docs/examples/simulator_initialization.md @@ -2,314 +2,230 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true execution_timeout: 300 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Configuring the Simulator -YAQS draws a sharp line between **what** you simulate and **how** it runs: - -| Layer | Role | -| ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| {class}`~mqt.yaqs.State`, {class}`~mqt.yaqs.Hamiltonian`, {class}`~mqt.yaqs.AnalogSimParams` / {class}`~mqt.yaqs.DigitalSimParams` | The physics: initial state, operator, time grid, observables, trajectory count, truncation. | -| {class}`~mqt.yaqs.NoiseModel` | Optional physics input supplied to {meth}`~mqt.yaqs.Simulator.run`. | -| {class}`~mqt.yaqs.Simulator` | The execution: parallel vs. serial trajectories, worker count, progress reporting, multiprocessing start method, and retry policy for transient worker errors. | - -This page walks through every option on the {class}`~mqt.yaqs.Simulator` class -so you can tune execution without touching the physics. - -```{code-cell} ipython3 -from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Result, Simulator, State -``` - -A small reusable analog problem we will simulate throughout: - -```{code-cell} ipython3 -L = 4 -H = Hamiltonian.ising(L, J=1.0, g=0.5) - - -def make_params(num_traj: int = 8) -> AnalogSimParams: - """A short Ising evolution measuring `` on every site.""" - return AnalogSimParams( - observables=[Observable("z", site) for site in range(L)], - elapsed_time=0.2, - dt=0.05, - num_traj=num_traj, - max_bond_dim=8, - svd_threshold=1e-9, - sample_timesteps=False, - random_seed=0, - ) - - -state = State(L, initial="zeros") -``` - -## Quick start: defaults - -Calling `Simulator()` with no arguments gives you parallel execution across most -of your CPU cores, a `tqdm` progress bar, an `"auto"` multiprocessing context, -and a generous retry policy. - -```{code-cell} ipython3 -sim = Simulator() -``` - -Every option is keyword-only, so you can override one without specifying the -others: - -```{code-cell} ipython3 -quiet_sim = Simulator(show_progress=False) -``` - -## Reusing one `Simulator` across runs - -A `Simulator` instance is **stateless** with respect to the physics; the same -instance can drive arbitrarily many {meth}`~mqt.yaqs.Simulator.run` calls. This -is the recommended pattern in scripts and notebooks because it keeps execution -configuration in one place. +`Simulator` controls how a calculation runs: parallel workers, progress bars, +and worker-error handling. Construct one instance and reuse it for successive +runs. The state, Hamiltonian or circuit, measurements, and noise are supplied +with each call; their settings are described in {doc}`simulation_parameters`. + +## Run a small noisy simulation + +This four-site Ising chain starts with every spin in $|1\rangle$. Local +relaxation acts during the evolution, and eight trajectories contribute to the +mean Pauli $Z$ expectations: + +```{code-cell} python +from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseModel, Observable, Simulator, State + +length = 4 +state = State(length, initial="ones") +hamiltonian = Hamiltonian.ising(length, J=1.0, g=0.5) +params = AnalogSimParams( + observables=[Observable("z", site) for site in range(length)], + elapsed_time=0.4, + dt=0.05, + num_traj=8, + random_seed=7, +) +noise = NoiseModel([ + {"name": "lowering", "sites": [site], "strength": 0.4} + for site in range(length) +]) -```{code-cell} ipython3 sim = Simulator(show_progress=False) - -for noise_strength in (0.0, 0.05, 0.1): - params = make_params() - result = sim.run(state, H, params, noise_model=None) -``` - -Each call constructs a short-lived `ProcessPoolExecutor` when `parallel=True`; -pools are not persisted across {meth}`~mqt.yaqs.Simulator.run` calls, so you can -safely change `sim.max_workers` (or replace `sim` entirely) between calls. - -## `parallel`: process-pool vs. in-process execution - -`parallel=True` (the default) runs trajectories in worker processes via -`concurrent.futures.ProcessPoolExecutor`. `parallel=False` runs every trajectory -in the calling process, which is useful for: - -- Debugging (full tracebacks, no pickling, breakpoints work). -- Very small jobs where the pool startup cost dominates. -- Notebook cells where you want to share state with the caller. - -Both modes produce identical results for a fixed `random_seed`: - -```{code-cell} ipython3 -import numpy as np - -params_serial = make_params() -sim_serial = Simulator(parallel=False, show_progress=False) -result_serial = sim_serial.run(state, H, params_serial) - -params_parallel = make_params() -sim_parallel = Simulator(parallel=True, max_workers=2, show_progress=False) -result_parallel = sim_parallel.run(state, H, params_parallel) -``` - -```{note} -For runs with `num_traj == 1` (e.g. noise-free analog/circuit dynamics, -Lindblad), the simulator automatically takes the in-process path even with -`parallel=True`. The pool is only spun up when there is more than one trajectory -to dispatch. -``` - -## `max_workers` and how the default is chosen - -When `max_workers` is left as `None`, the simulator picks -`max(1, available_cpus() - 1)` to leave one core free for the parent process and -the OS: - -```{code-cell} ipython3 -from mqt.yaqs.simulator import available_cpus - -cpus = available_cpus() -default_workers = Simulator().max_workers -``` - -{func}`~mqt.yaqs.core.parallel_utils.available_cpus` (re-exported as -{func}`~mqt.yaqs.simulator.available_cpus`) is deliberately cgroup- and -scheduler-aware. In priority order it honours: - -1. `YAQS_MAX_WORKERS` (explicit user override; positive integer). -2. `PYTEST_XDIST_WORKER` (returns `1` to avoid nested parallelism in tests). -3. SLURM hints — `SLURM_CPUS_PER_TASK` then `SLURM_CPUS_ON_NODE`. -4. Linux `os.sched_getaffinity(0)` (respects `taskset`, containers, cgroups). -5. `os.cpu_count()` as a final fallback. - -Override the resolution either by setting the environment variable… - -```python -# In a shell: export YAQS_MAX_WORKERS=4 +result = sim.run(state, hamiltonian, params, noise_model=noise) ``` -…or by passing `max_workers` explicitly: - -```{code-cell} ipython3 -sim_four = Simulator(max_workers=4, show_progress=False) -``` +Parallel execution remains enabled. The documentation suppresses progress bars +with `show_progress=False`; use `Simulator()` to see progress in your own runs. +The small trajectory budget illustrates execution and result access, rather than +sampling convergence. -## `show_progress`: tqdm bars +## Read the result and reuse the simulator -`show_progress=True` (default) shows a `tqdm` bar labelled "Running -trajectories" (or "Running unitary ensemble" for the deterministic ensemble -path). Set `show_progress=False` to silence it — useful in test suites, batch -scripts, and CI logs: +`run` returns a `Result`. Observable order matches the supplied list: -```{code-cell} ipython3 -silent = Simulator(show_progress=False) -silent.run(state, H, make_params(num_traj=4)) +```{code-cell} python +times = result.times +z0_mean = result.expectation_values[0] +z0_trajectories = result.trajectories[0] ``` -The bar is suppressed regardless of `parallel`, so the same flag also silences -serial runs. - -## `mp_context`: multiprocessing start method +Here, `times` and `z0_mean` have nine entries, including time zero. +`z0_trajectories` has shape `(8, 9)`: one row per trajectory and one column per +time. `z0_mean` averages those rows. Circuit runs can also return readout counts +in `result.counts`; see {doc}`circuit_shots`. -`mp_context` controls how worker processes start. The default `"auto"` selects a -start method per OS: +Reuse the same simulator and inputs for a noiseless reference: -| Value | Behaviour | -| --------- | --------------------------------------------------------------------------- | -| `"auto"` | `"forkserver"` on Linux, `"spawn"` everywhere else. | -| `"fork"` | Copies the parent process. Avoid when the parent has active threads. | -| `"spawn"` | Fresh interpreter per worker. Used on Windows/macOS and available on Linux. | - -On Linux, `"auto"` creates workers through a separate server process. The first -pool has extra startup cost, but workers do not fork the application's threaded -process. Numerical thread limits control resource use; they do not make an -explicit `"fork"` safe when the parent has active threads. - -```{code-cell} ipython3 -automatic_context = Simulator(mp_context="auto") -portable_context = Simulator(mp_context="spawn") +```{code-cell} python +reference = sim.run(state, hamiltonian, params) ``` -If you mix YAQS with GPU libraries or anything that does not survive `fork()`, -force `mp_context="spawn"`. +Each run starts from the supplied initial state. It does not continue from the +previous result. Sequential calls share execution settings, but they do not keep +a worker pool alive. YAQS creates a pool only when the calculation has multiple +independent jobs and more than one worker is available. -## `max_retries` and `retry_exceptions` +## Choose the common execution controls -Long parallel runs occasionally encounter transient worker failures: a worker is -cancelled by the OS, a `TimeoutError` is raised, or a transient `OSError` (e.g. -a temporary file system hiccup) propagates out of a backend. By default, the -simulator retries each failing trajectory up to **10 times** for the following -exception types: +All constructor options are keyword-only. Leave the defaults in place unless you +need a specific execution budget or a quieter run. -- `concurrent.futures.CancelledError` -- `TimeoutError` -- `OSError` +| Option | Default | When to change it | +| --------------- | --------- | --------------------------------------------------------------------------------------- | +| `show_progress` | `True` | Set `False` to suppress trajectory and readout bars in documentation or logs. | +| `max_workers` | Automatic | Set a positive integer to cap worker processes, for example `Simulator(max_workers=4)`. | +| `parallel` | `True` | Set `False` to debug in the calling process or avoid process startup for a small job. | -```{code-cell} ipython3 -default_retries = Simulator().max_retries -retry_types = Simulator().retry_exceptions -``` +A pool requires `parallel=True`, more than one independent job, and +`max_workers > 1`. Noiseless single-state evolution and density-matrix evolution +run in the calling process. `max_workers=1` also keeps execution in that +process, even when parallel execution is enabled, and limits numerical threads +to one. Worker processes limit their own numerical threads to avoid multiplying +thread pools across CPUs. -Tighten the policy for fail-fast development (e.g. when bisecting an error): +You can change settings between calls, for example `sim.max_workers = 2` or +`sim.show_progress = True`. Setting `sim.max_workers = None` restores automatic +worker selection. -```{code-cell} ipython3 -strict = Simulator(max_retries=0, show_progress=False) -``` +## Run from a Python script -Or broaden it for unreliable environments: +Worker processes start through `forkserver` on Linux and `spawn` on Windows and +macOS by default. These methods need a script entry-point guard so importing the +script in a child process does not start the simulation again. -```{code-cell} ipython3 -import concurrent.futures +Keep the imports and input definitions from the first example. Replace its +simulator creation and run calls with this block, and keep subsequent run calls +inside the guard: -resilient = Simulator( - max_retries=20, - retry_exceptions=(concurrent.futures.CancelledError, TimeoutError, OSError, ConnectionError), - show_progress=False, -) -``` +```python +if __name__ == "__main__": + sim = Simulator() + result = sim.run(state, hamiltonian, params, noise_model=noise) + reference = sim.run(state, hamiltonian, params) +``` + +Run the file with `python your_script.py`. In a notebook, execute the cells in +order without adding this guard. If process startup is the cause of a debugging +problem, `parallel=False` lets you inspect the calculation in the notebook's +process. -Permanent errors (e.g. `ValueError` from your physics setup, `AssertionError` -from invariants) are not retried — they propagate after the first failure -regardless of `max_retries`. +## Advanced execution options -## Inspecting the return value: `Result` +:::{dropdown} Automatic worker budgets and numerical threads -{meth}`~mqt.yaqs.Simulator.run` returns a {class}`~mqt.yaqs.Result` that holds -every simulation output through a small, stable surface. The -{class}`~mqt.yaqs.core.data_structures.simulation_parameters.AnalogSimParams` -you passed in is referenced unchanged at `result.sim_params`: +With `max_workers=None`, the simulator uses `max(1, available_cpus() - 1)`. CPU +discovery takes the first valid hint from: + +1. `YAQS_MAX_WORKERS`. +2. A pytest-xdist worker, which reports one CPU to avoid nested pools. +3. `SLURM_CPUS_PER_TASK`, then `SLURM_CPUS_ON_NODE`. +4. Process CPU affinity, when available. +5. The operating system's CPU count. + +Invalid or non-positive environment hints are ignored. The default worker policy +then leaves one reported CPU free. Thus `YAQS_MAX_WORKERS=4` normally resolves +to three workers; `Simulator(max_workers=4)` explicitly permits four. Use the +constructor argument when you need an exact process cap. CPU affinity can +restrict the available cores, but this discovery does not read every container +CPU-quota setting. + +A worker cap bounds process count, not total memory. Each process needs its own +simulation state. Reduce the cap when several concurrent trajectories exceed +your memory budget. Numerical libraries are capped inside workers; turning off +process parallelism does not promise unrestricted BLAS threading. + +::: + +:::{dropdown} Multiprocessing start methods + +`mp_context="auto"` selects `"forkserver"` on Linux and `"spawn"` elsewhere. For +an explicit selection, the public options are `"spawn"` and `"fork"` on +platforms that support them. There is no fallback for an unavailable method. +Keep `"auto"` unless the environment requires a specific method. + +`"spawn"` starts a fresh interpreter for each worker. It can also be useful when +combining YAQS with libraries that need fresh process initialization. `"fork"` +copies the application's process and can be unsafe when the parent has active +threads. Thread limits do not remove that risk. The automatic Linux context +starts workers from a separate server process; its first pool has additional +startup cost. + +Worker inputs must be pickleable. Pools are scoped to individual runs, although +the Python forkserver helper can remain alive between calls. These execution +choices do not change the requested simulation model. + +::: + +:::{dropdown} Retries for worker errors + +`max_retries=10` allows up to ten additional attempts for a failed job in a +process pool. The default `retry_exceptions` tuple contains +`concurrent.futures.CancelledError`, `TimeoutError`, and `OSError`. + +Only matching exceptions raised when retrieving a worker result trigger a retry. +After the retry budget is exhausted, the error propagates. Other exceptions, +including `ValueError`, propagate immediately under the default policy. Set +`max_retries=0` to propagate the first worker failure, or supply a tuple of +exception classes for a specific transient failure in your environment. + +Retries do not impose a timeout, apply to in-process execution, or restart a +broken pool. Pool startup and submission failures are outside this retry policy. +Do not increase the retry budget to handle a repeatable error in the model. + +::: + +:::{dropdown} Other result fields and configuration references + +Outputs that do not apply to a run remain `None` or empty. The API reference for +{class}`~mqt.yaqs.Result` describes the full result structure: + +| Fields | Use | +| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `observables`, `expectation_values`, `trajectories` | Requested observables, their means, and individual realization data in the supplied order. | +| `times` | Analog sample times, or the stitched timeline of a program with observables. Standalone circuits have no physical time axis. | +| `counts`, `measurements` | Total circuit readout counts and per-trajectory histograms when shots are requested. | +| `output_state` | A final state when `get_state=True` is supported; see {doc}`simulation_parameters`. | +| `max_bond`, `total_bond`, `runtime_cost` | MPS bond diagnostics and an estimated contraction cost, when recorded. | +| `multi_time_times`, `multi_time_results` | Complex two-time correlations for unitary ensembles; see {doc}`ensemble_evolution`. | +| `segment_results` | Individual results from a `SimulationProgram`; see {doc}`digital_analog_simulation`. | +| `sim_params`, `noise_model` | The validated simulation parameters and the noise model used by the run. | + +For a standalone run, `result.sim_params` references the supplied parameter +object. Validation normalizes that object and rebuilds its analog time grid; it +is not an immutable snapshot. Construct a new parameter object when you need to +retain a separate configuration. State and Hamiltonian wrappers may also +populate cached representations, while evolution uses copies of their input +states. + +A program's top-level `sim_params` is `None`; read segment parameters through +`result.segment_results`. Program settings and trajectory budgets are explained +in {doc}`digital_analog_simulation`. -```{code-cell} ipython3 -sim = Simulator(show_progress=False) -params = make_params() -result = sim.run(state, H, params) -``` - -The properties that don't apply to your simulation kind return `None` (or an -empty list for `observables` when only shots were requested), so you can branch -on them safely. The full set is: - -| Property | Populated for | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `observables` | Analog and digital runs with observables. Empty list for all digital runs without observables (shots-only and state-only, e.g. `get_state=True`). | -| `expectation_values` | Aggregated expectation per observable (parallel to `observables`). | -| `trajectories` | Per-trajectory data per observable (parallel to `observables`). | -| `times` | Shared analog time grid; for digital–analog programs, the stitched physical timeline when observables are configured (`None` for shots-only / no-observable programs and standalone digital circuits). | -| `runtime_cost` | MPS-backed analog and digital runs that are not shots-only (contraction-cost heuristic over time). | -| `max_bond` | MPS-backed analog and digital runs that are not shots-only (maximum bond dimension over time). | -| `total_bond` | MPS-backed analog and digital runs that are not shots-only (sum of internal bond dimensions). | -| `noise_model` | Any run that was given a `NoiseModel`; otherwise `None`. | -| `output_state` | Runs with `get_state=True` on `AnalogSimParams` or `DigitalSimParams`. For Lindblad (`density_matrix`), noisy runs are supported; for `mps`/`vector`, noiseless only. | -| `multi_time_times`, `multi_time_results` | Analog deterministic ensembles with `multi_time_observables` set. | -| `counts` | Digital runs with `shots` set (the `dict[int, int]` of aggregated measurement outcomes). | - -A digital–analog {class}`~mqt.yaqs.SimulationProgram` returns a top-level -{class}`~mqt.yaqs.Result` whose `sim_params` is `None`; read each segment's -parameters from `result.segment_results[i].sim_params`. When observables are -configured, `result.times` and `result.expectation_values` are stitched onto the -physical program timeline; shots-only and other no-observable programs leave -`result.times` as `None`. Observables and `random_seed` are set on the program -(or as keyword arguments when passing a pair list to -{meth}`~mqt.yaqs.Simulator.run`). Prefer setting `num_traj` the same way; when -{attr}`~mqt.yaqs.SimulationProgram.num_traj` is omitted (`None`), execution -falls back to the `num_traj` value specified consistently on every segment's -parameter object. Conflicting segment values require an explicit program-level -`num_traj`. A program that requests shots but no observables follows standalone -digital semantics and executes one complete-program stochastic trajectory per -shot. - -`Result` (and its wrapped `sim_params`) is pickleable, so you can checkpoint and -resume analysis from disk: - -```{code-cell} ipython3 -import pickle - -blob = pickle.dumps(result) -restored: Result = pickle.loads(blob) # noqa: S301 -``` +For temporary storage, pickle can save results for later analysis with matching +Python, YAQS, and dependency versions. This stores the result; it does not +resume an interrupted simulation. Only load pickle files from a trusted source. + +::: + +## Related guides -## Choosing settings for common scenarios - -| Scenario | Recommended `Simulator(...)` | -| ------------------------------------------------- | ---------------------------------------------------------------------- | -| Quick local run, want to see a progress bar | `Simulator()` | -| Notebook / docs build / CI logs | `Simulator(show_progress=False)` | -| Debugging a physics setup | `Simulator(parallel=False, show_progress=False)` | -| Single-process benchmark, all cores in the worker | `Simulator(parallel=False)` and let BLAS/OpenMP use all threads | -| Fixed core budget (e.g. SLURM job step) | `YAQS_MAX_WORKERS=N` in the environment, or `Simulator(max_workers=N)` | -| Mixing with GPU / non-fork-safe code | `Simulator(mp_context="spawn")` | -| Long unattended run on a flaky cluster | `Simulator(max_retries=20)` with broadened `retry_exceptions` | - -For physics-side settings (`num_traj`, `max_bond_dim`, `svd_threshold`, -`random_seed`, `sample_timesteps`, observables, noise), see -{doc}`analog_simulation`, {doc}`representation_comparison`, and -{doc}`state_initialization`. - -## Related topics - -- {doc}`quickstart` — minimal first simulation -- {doc}`simulation_parameters` — physics-side presets and truncation -- {doc}`analog_simulation` — TJM workflow with noise -- {doc}`circuit_observables` — circuit observables, mid-circuit sampling, gate - modes +- {doc}`simulation_parameters` — accuracy, sampling budgets, and output + requests. +- {doc}`analog_simulation` — noisy analog dynamics. +- {doc}`circuit_observables` — circuit expectations and checkpoints. +- {doc}`digital_analog_simulation` — analog-digital programs and segment + results. +- {doc}`representation_comparison` — choosing the analog state representation. + +Full constructor and `run` signatures are in the API reference for +{class}`~mqt.yaqs.Simulator`. diff --git a/docs/examples/state_initialization.md b/docs/examples/state_initialization.md index 6bb209ed4..9e307dcfa 100644 --- a/docs/examples/state_initialization.md +++ b/docs/examples/state_initialization.md @@ -2,228 +2,211 @@ file_format: mystnb kernelspec: name: python3 +language_info: + name: python mystnb: number_source_lines: true execution_timeout: 300 --- -```{code-cell} ipython3 -:tags: [remove-cell] -%config InlineBackend.figure_formats = ['svg'] -``` - # Initializing Quantum States -YAQS separates **what you specify** (a -{class}`~mqt.yaqs.core.data_structures.state.State`) from **how evolution runs** -({class}`~mqt.yaqs.core.data_structures.simulation_parameters.AnalogSimParams`, -Hamiltonian, noise). +The initial state sets the starting point for a YAQS simulation. Use `State` to +choose a named preparation or supply your own data. The default matrix product +state (MPS) representation supports analog and circuit simulation without +storing a full state vector. -| Layer | Role | -| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`State`** | User-facing initial condition: length, preset name, optional raw data, and **which representation** to evolve in (`"mps"`, `"vector"`, or `"density_matrix"`). | -| **`MPS`** | Internal tensor network; used by the simulator when needed. Prefer {class}`~mqt.yaqs.core.data_structures.state.State` in application code. | +## Choose a product state -**Workflow:** build a {class}`~mqt.yaqs.core.data_structures.state.State` and a -{class}`~mqt.yaqs.core.data_structures.hamiltonian.Hamiltonian` once (both -materialize at construction), then pass them to -{meth}`Simulator.run ` — including in parameter loops. +Specify the number of sites and, optionally, a preset. Sites are qubits unless +you provide other local dimensions: -```{code-cell} ipython3 +```{code-cell} python from mqt.yaqs import State -preset = State(4, initial="x+") - -mcwf_state = State(4, initial="zeros", representation="vector") +zeros = State(20) +polarized = State(20, initial="x+") ``` -## `State` versus `MPS` - -Use `State` in {meth}`Simulator.run `. Use -{class}`~mqt.yaqs.core.data_structures.mps.MPS` directly only for low-level -tensor-network code, or wrap an existing MPS with {meth}`State.from_mps` -`. +Here, `zeros` puts every site in $|0\rangle$, while `polarized` puts every site +in $(|0\rangle + |1\rangle)/\sqrt{2}$. Both are product states: the sites have +no initial entanglement. + +| `initial` | Preparation | +| ------------------- | ------------------------------------------------------------------------------------------------ | +| `"zeros"` (default) | Every site in $\lvert 0\rangle$. | +| `"ones"` | Every site in $\lvert 1\rangle$. | +| `"x+"`, `"x-"` | Every site in $(\lvert 0\rangle \pm \lvert 1\rangle)/\sqrt{2}$. | +| `"y+"`, `"y-"` | Every site in $(\lvert 0\rangle \pm i\lvert 1\rangle)/\sqrt{2}$. | +| `"Neel"` | Alternating levels, starting with site 0 in $\lvert 1\rangle$: `1010…` in site order. | +| `"wall"` | The first `length // 2` sites in $\lvert 0\rangle$ and the remaining sites in $\lvert 1\rangle$. | +| `"basis"` | One computational-basis configuration, supplied with `basis_string`. | +| `"random"` | A random product state; see the random-state examples below. | + +## Place an excitation and check site order + +The {doc}`analog_simulation` guide follows an excitation that starts near the +center of a chain. Prepare that state by giving one basis digit per site: + +```{code-cell} python +length = 20 +center = length // 2 +basis = "0" * center + "1" + "0" * (length - center - 1) +localized = State(length, initial="basis", basis_string=basis) +``` -**Circuit simulation** requires `representation="mps"` (the preset default). -`Simulator.run` with `DigitalSimParams` rejects vector and density-matrix -states. +Character `i` in `basis_string` selects site `i`, starting with site 0 on the +left. A measurement bitstring instead displays site 0 on the right: -## How `representation` is chosen +```{code-cell} python +site_zero = State(3, initial="basis", basis_string="100") +print(site_zero.mps.to_vec()) +``` -| How you build `State` | `representation` | -| ------------------------------------- | ------------------------------------------------------------------------------ | -| Preset only (`length`, `initial=`, …) | Default `"mps"`; override with `representation="vector"` or `"density_matrix"` | -| `tensors=` (MPS cores) | Inferred `"mps"` — do **not** pass `representation=` | -| `vector=` | Inferred `"vector"` | -| `density_matrix=` | Inferred `"density_matrix"` | +This state has site 0 in $|1\rangle$ and the other sites in $|0\rangle$. Its +dense vector has its only nonzero entry at index 1, and its readout bitstring is +`"001"`. Dense vectors and the rows and columns of density matrices use site 0 +as the fastest-varying subsystem. For qubits, this matches Qiskit's ordering. -```{code-cell} ipython3 -import numpy as np +## Choose a representation -vec = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.complex128) -from_vector = State(vector=vec) +Set `representation` when constructing a preset state, for example +`State(4, initial="x+", representation="vector")`. YAQS selects the analog +backend from this choice: -lindblad_ready = State(2, initial="zeros", representation="density_matrix") -``` +| `representation` | State storage and supported use | +| ------------------ | ---------------------------------------------------------------------------------- | +| `"mps"` (default) | Tensor network for analog evolution, circuits, and unitary ensembles. | +| `"vector"` | Dense pure state for analog evolution with Monte Carlo wave-function trajectories. | +| `"density_matrix"` | Dense pure or mixed state for analog Lindblad evolution. | -## Preset product states +For $N$ qubits, a dense vector has $2^N$ entries and a density matrix has $4^N$ +entries. Keep dense calculations small; circuit simulation requires `"mps"`. See +{doc}`representation_comparison` for a comparison on the same physical model. -Presets match `MPS(..., state=...)` names: `"zeros"`, `"ones"`, `"x+"`, -`"Neel"`, `"wall"`, `"basis"`, `"random"`, etc. +## Prepare random states -For **MCWF** or **Lindblad**, set `representation` on the `State` and call -`Simulator.run` — no extra steps: +Use `"random"` for an unentangled state with real, nonnegative random local +amplitudes. To allow initial entanglement, use `"haar-random"` and choose an +initial maximum bond dimension with `pad`: -```{code-cell} ipython3 -neel_mcwf = State(4, initial="Neel", representation="vector") +```{code-cell} python +random_product = State(6, initial="random") +random_mps = State(6, initial="haar-random", pad=4) ``` -Product presets can evolve in dense form without ever building an MPS in memory. -**Entangled** presets (e.g. `"haar-random"`) may still require an internal MPS -when you choose a dense representation. +`"haar-random"` builds an MPS from random isometries. It does not sample +uniformly from all pure states in the full Hilbert space. The bond dimensions +are limited by `pad` and the sizes of the neighboring subsystems. Omitting `pad` +gives a maximum bond dimension of one, so the state is then a product state. +This initial choice is separate from `max_bond_dim` during evolution. -Reproducible `"random"` presets: pass `seed=` on `State`. +For a reproducible random product state in a dense representation, pass `seed`: -```{code-cell} ipython3 -a = State(3, initial="random", seed=7, representation="vector") -b = State(3, initial="random", seed=7, representation="vector") -# Same specification; run() will evolve both consistently. +```{code-cell} python +repeatable = State(4, initial="random", representation="vector", seed=7) ``` -## Manual initialization - -Pass **exactly one** of `tensors`, `vector`, or `density_matrix`. Representation -is **inferred**; do not pass `representation=`. Preset-only kwargs (`initial`, -`pad`, `basis_string`, `seed`) cannot be combined with manual data. - -### MPS cores (`tensors=`) - -```{code-cell} ipython3 -from mqt.yaqs import MPS - -mps_ref = MPS(3, state="zeros") -spec = State(tensors=list(mps_ref.tensors)) -``` +The same seed reproduces the same initial vector. It also works with +`representation="density_matrix"`, which forms the corresponding pure-state +density matrix. Currently, random MPS initialization and `"haar-random"` ignore +`seed`. To reuse those preparations, construct the state once and pass the same +object to successive runs. The simulation parameter `random_seed` controls +stochastic evolution; it does not seed state preparation. -### Dense state vector (`vector=`) +## Supply a vector or a mixed state -`length` is inferred when the Hilbert-space dimension is a power of two. +Pass exactly one of `vector`, `density_matrix`, or `tensors` for manual data. +The representation is inferred, so omit `representation`. Do not combine manual +data with preset options such as `initial`, `basis_string`, `seed`, or `pad`. -```{code-cell} ipython3 -vec = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.complex128) # |00> -spec = State(vector=vec) -``` +The vector below prepares the entangled Bell state +$(|00\rangle + |11\rangle)/\sqrt{2}$: -### Density matrix (`density_matrix=`) +```{code-cell} python +import numpy as np -```{code-cell} ipython3 -rho = np.diag([1.0, 0.0, 0.0, 0.0]).astype(np.complex128) -spec = State(density_matrix=rho) +bell = State(vector=np.array([1, 0, 0, 1], dtype=complex)) +print(bell.vector) ``` -A `State` created only with `vector=` or `density_matrix=` cannot be used for -circuit simulation; use `tensors=` or a preset with `representation="mps"` -instead. +YAQS copies and normalizes the vector. The input must be a finite, nonzero, +one-dimensional array. Without explicit local dimensions, its size must be a +power of two; YAQS infers the number of qubits. -## Representation and backends (analog) +A mixed state describes a statistical preparation. This example puts the system +in $|00\rangle$ with probability 0.6 and $|11\rangle$ with probability 0.4: -Set **`representation` on `State`**, not on `AnalogSimParams`. -{meth}`Simulator.run ` materializes the correct internal -form and dispatches: - -| `representation` | Backend (analog) | -| ------------------ | ------------------------------------- | -| `"mps"` (default) | TJM (`analog_tjm_1` / `analog_tjm_2`) | -| `"vector"` | MCWF | -| `"density_matrix"` | Lindblad (small systems) | - -### Default: MPS / TJM +```{code-cell} python +rho = np.diag([0.6, 0.0, 0.0, 0.4]).astype(complex) +mixed = State(density_matrix=rho) +``` -```{code-cell} ipython3 -from mqt.yaqs import Hamiltonian, MPS, Simulator, AnalogSimParams, Observable +YAQS copies the matrix and normalizes its trace. The input must be finite, +square, Hermitian, positive semidefinite, and have positive real trace. Manual +vectors and density matrices select their dense analog backends. They cannot +serve as circuit inputs; use a preset or MPS tensors instead. -sim = Simulator(show_progress=False) +## Use other local dimensions -L = 3 -H = Hamiltonian.ising(L, J=1.0, g=0.5) -obs = Observable("z", sites=[0]) +Set `physical_dimensions` to an integer for a uniform chain or a list for +different dimensions at each site: -state_mps = State(L, initial="zeros") -params = AnalogSimParams( - observables=[obs], - elapsed_time=0.2, - dt=0.05, +```{code-cell} python +qutrits = State(3, physical_dimensions=3) +qubit_qutrit = State( + 2, initial="basis", basis_string="12", physical_dimensions=[2, 3] ) -result = sim.run(state_mps, H, params, noise_model=None) ``` -### MCWF (`representation="vector"`) +The second state has a qubit at site 0 in level 1 and a qutrit at site 1 in +level 2. Presets such as `"ones"` still use level 1, not the highest local +level. For manual dense data, local dimensions must multiply to the vector +length or matrix dimension. Bitstring probabilities and shot measurements +require qubits; digital gates currently require qubit target sites. See +{doc}`transmon_emulation` and {doc}`trapped_ion` for device examples. -For guidance on choosing a representation, see {doc}`representation_comparison`. +## Advanced MPS preparation -```{code-cell} ipython3 -state_vec = State(L, initial="zeros", representation="vector") -obs_vec = Observable("z", sites=[0]) -params_vec = AnalogSimParams( - observables=[obs_vec], - elapsed_time=0.2, - dt=0.05, -) -result = sim.run(state_vec, H, params_vec, None) +:::{dropdown} Supply MPS tensors or wrap an existing MPS + +Use `tensors=` for custom open-boundary MPS cores. Each core has axes +`(physical, left, right)`. Neighboring bond dimensions must match, exterior +bonds must have dimension one, and all entries must be finite. This example +prepares the same Bell state as the vector above, now in MPS form: + +```python +left = (np.eye(2) / np.sqrt(2)).reshape(2, 1, 2) +right = np.eye(2).reshape(2, 2, 1) +bell_mps = State(tensors=[left, right]) +wrapped = State.from_mps(bell_mps.mps) ``` -### Lindblad (`representation="density_matrix"`) +`State(tensors=...)` infers the site count and normalizes the MPS. For other +physical dimensions, supply matching `physical_dimensions`. -For guidance on choosing a representation, see {doc}`representation_comparison`. +When you already have an {class}`~mqt.yaqs.MPS`, use +{meth}`~mqt.yaqs.State.from_mps` to wrap it. This method references the same MPS +without copying or normalizing it. Changes through either reference affect the +same state. For an independent normalized state, supply copies of its tensors +through `State(tensors=...)` and preserve its physical dimensions. -```{code-cell} ipython3 -state_dm = State(L, initial="zeros", representation="density_matrix") -obs_dm = Observable("z", sites=[0]) -params_dm = AnalogSimParams( - observables=[obs_dm], - elapsed_time=0.2, - dt=0.05, -) -result = sim.run(state_dm, H, params_dm, None) -``` +::: -See {doc}`representation_comparison` for a side-by-side comparison of the three -representations on the same Hamiltonian. +:::{dropdown} Pad the initial MPS bonds -### Passing dense data directly +For product presets, `pad` adds zero entries to enlarge the initial MPS bonds +without changing the physical state. For `"haar-random"`, it instead sets the +maximum initial bond dimension used during random construction. -If you already have $|\psi\rangle$ or $\rho$, pass `vector=` or -`density_matrix=` — representation is inferred: +Padding allocates initial MPS space; it does not set the bond limit during +evolution. That limit is `max_bond_dim` in the simulation parameters. Dense +product-state construction does not use MPS padding. -```{code-cell} ipython3 -psi = np.zeros(2**L, dtype=np.complex128) -psi[0] = 1.0 -state_from_vec = State(vector=psi) -result = sim.run(state_from_vec, H, params_vec, None) -``` +::: -## Practical limits - -- **Memory**: dense `vector` scales as $2^N$; `density_matrix` as $2^{2N}$. - Prefer `representation="mps"` for longer chains. -- **Entangled presets**: `"haar-random"` may need an internal MPS for dense - representations. -- **Circuits**: use `State(..., representation="mps")` (default); `vector=` / - `density_matrix=` states cannot run circuits. -- **Ensemble runs**: `list[State]` for deterministic unitary ensembles requires - each member with `representation="mps"`. -- **`get_state`**: when supported, `result.output_state` is a - {class}`~mqt.yaqs.core.data_structures.state.State`. Use `.mps` for MPS runs, - `.vector` for MCWF, or `.density_matrix` for Lindblad. Not supported with - stochastic noise on `mps` or `vector` representations (use `density_matrix` - for the exact ensemble average). - -For MPO/TJM details without `State`, see {doc}`analog_simulation` and the -{class}`~mqt.yaqs.core.data_structures.mps.MPS` API reference. - -## Related topics - -- {doc}`quickstart` — minimal first simulation -- {doc}`representation_comparison` — MPS, MCWF, and Lindblad backends -- {doc}`analog_simulation` — TJM evolution workflow -- {doc}`simulation_parameters` — presets and trajectory settings +Once the initial state is prepared, pass it to `Simulator.run` with the model, +measurements, and optional noise. The {doc}`simulator_initialization` guide +shows how to run and reuse a simulator, while {doc}`simulation_parameters` +describes accuracy and sampling choices. Full constructor details are in the +{class}`~mqt.yaqs.State` API reference. diff --git a/docs/index.md b/docs/index.md index 1acd0b8e4..8ac8757ba 100644 --- a/docs/index.md +++ b/docs/index.md @@ -62,7 +62,7 @@ flowchart LR | Define custom single-site jump operators | {doc}`examples/realistic_noise_models` | | Compare scalable MPS, MCWF, and Lindblad analog paths | {doc}`examples/representation_comparison` | | Two-time correlations and typicality ensembles | {doc}`examples/ensemble_evolution` | -| Scheduled jumps at fixed times | {doc}`examples/scheduled_jumps` | +| Scheduled jumps at fixed times | {ref}`noise-scheduled-jumps` | | Transfer an excitation between superconducting qubits | {doc}`examples/transmon_emulation` | | Transport a trapped ion and study motional noise | {doc}`examples/trapped_ion` | | Characterize environmental memory effects via probing the process | {doc}`examples/characterization` | @@ -74,7 +74,7 @@ flowchart LR | Get hardware-like shot histograms | {doc}`examples/circuit_shots` | | Combine analog evolution and digital operations in one program | {doc}`examples/digital_analog_simulation` | | Verify two circuits are equivalent | {doc}`examples/equivalence_checking` | -| Custom gate translation | {doc}`examples/custom_gates` | +| Supply custom circuit gates | {ref}`circuit-custom-gates` | ```{toctree} :caption: Start here @@ -140,8 +140,6 @@ Circuit verification :titlesonly: Ensemble evolution -Scheduled jumps -Custom gates Non-Markovian transformer models (experimental) ``` From a7f08a6b55e43ab1cae0410239d601dfa98c0593 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:59:34 +0200 Subject: [PATCH 20/30] condensed front page table --- docs/index.md | 60 +++++++++++---------------------------------------- 1 file changed, 12 insertions(+), 48 deletions(-) diff --git a/docs/index.md b/docs/index.md index 8ac8757ba..36dfba24a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,54 +27,18 @@ self ## User guide -MQT YAQS targets workloads that need **scale and efficiency**: large noisy -circuits, long analog time evolution, and hardware models with many degrees of -freedom. For smaller systems, **MCWF** (`vector`) and **Lindblad** -(`density_matrix`) analog backends are available as well; see -{doc}`examples/representation_comparison`. - -The pages below are **executable notebooks**: code cells run during the -documentation build, so examples stay in sync with the library. New users should -start with {doc}`installation`, then {doc}`examples/quickstart`. - -```{mermaid} -flowchart LR - state[State] - op[Hamiltonian or QuantumCircuit] - params["AnalogSimParams / DigitalSimParams"] - sim[Simulator] - result[Result] - state --> sim - op --> sim - params --> sim - sim --> result -``` - -### Find a guide - -| I want to… | Read | -| -------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| Run my first simulation in under a minute | {doc}`examples/quickstart` | -| Configure truncation, presets, and trajectories | {doc}`examples/simulation_parameters` | -| Build Hamiltonians (Pauli, Hubbard, transmon, trapped ion, …) | {doc}`examples/hamiltonians` | -| Simulate open-system (analog) dynamics with noise | {doc}`examples/analog_simulation` | -| Model realistic noise (log-normal and other distributions) | {doc}`examples/realistic_noise_models` | -| Define custom single-site jump operators | {doc}`examples/realistic_noise_models` | -| Compare scalable MPS, MCWF, and Lindblad analog paths | {doc}`examples/representation_comparison` | -| Two-time correlations and typicality ensembles | {doc}`examples/ensemble_evolution` | -| Scheduled jumps at fixed times | {ref}`noise-scheduled-jumps` | -| Transfer an excitation between superconducting qubits | {doc}`examples/transmon_emulation` | -| Transport a trapped ion and study motional noise | {doc}`examples/trapped_ion` | -| Characterize environmental memory effects via probing the process | {doc}`examples/characterization` | -| Study how long environmental memory persists in a system | {ref}`Memory persistence ` in {doc}`examples/characterization` | -| Train a surrogate and predict how a system evolves under control sequences | {doc}`examples/memory_surrogate` | -| Build a Markovian noise digital twin from measured trajectories | {doc}`examples/digital_twin` | -| Validate predictions at short temporal horizons with exact references | {doc}`examples/memory_surrogate` | -| Simulate a circuit and read observables | {doc}`examples/circuit_observables` | -| Get hardware-like shot histograms | {doc}`examples/circuit_shots` | -| Combine analog evolution and digital operations in one program | {doc}`examples/digital_analog_simulation` | -| Verify two circuits are equivalent | {doc}`examples/equivalence_checking` | -| Supply custom circuit gates | {ref}`circuit-custom-gates` | +Start with installation and the quickstart, then choose a guide for your task. +The examples include working code and plots. + +| Section | Guides | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Start here | {doc}`Installation ` · {doc}`Quickstart ` | +| Simulation setup | {doc}`Quantum states ` · {doc}`Hamiltonians ` · {doc}`Noise models ` · {doc}`State representations ` · {doc}`Simulation parameters ` · {doc}`Simulator configuration ` | +| Simulation workflows | {doc}`Analog ` · {doc}`Digital circuits ` · {doc}`Shot-based circuits ` · {doc}`Analog-digital ` | +| Emulation | {doc}`Superconducting qubits ` · {doc}`Trapped ions ` | +| Characterization and verification | {doc}`Environmental memory ` · {doc}`Noise characterization ` · {doc}`Circuit verification ` | +| Advanced examples | {doc}`Ensemble evolution ` · {doc}`Non-Markovian surrogate models (experimental) ` | +| Reference and contributing | {doc}`API ` · {doc}`Citations ` · {doc}`Changelog ` · {doc}`Upgrade guide ` · {doc}`Contributing ` · {doc}`Support ` | ```{toctree} :caption: Start here From 126b808950fbc3d6da58193a0a3b42bdcfefb9ab Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:56:39 +0200 Subject: [PATCH 21/30] fixed some factual statements --- docs/examples/circuit_shots.md | 5 +++-- docs/examples/digital_twin.md | 4 +++- docs/examples/equivalence_checking.md | 5 ++++- docs/examples/representation_comparison.md | 1 + docs/examples/simulation_parameters.md | 5 +++-- docs/examples/trapped_ion.md | 8 ++++++-- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/examples/circuit_shots.md b/docs/examples/circuit_shots.md index 02929db4a..62d1a896e 100644 --- a/docs/examples/circuit_shots.md +++ b/docs/examples/circuit_shots.md @@ -97,8 +97,9 @@ damped = simulator.run(state, circuit, params, noise) ``` Parallel execution remains enabled by default. `show_progress=False` suppresses -bars in the documentation; omit it to see progress. The seed makes the sampling -repeatable for the same configuration, but does not reduce sampling error. +bars in the documentation; omit it to see progress. The seed repeats the random +streams for jumps and sampled disorder for the same configuration, but does not +seed final readout sampling. Shot counts can therefore vary between runs. ## 4. Read individual outcomes diff --git a/docs/examples/digital_twin.md b/docs/examples/digital_twin.md index 99d5278ca..c494f170f 100644 --- a/docs/examples/digital_twin.md +++ b/docs/examples/digital_twin.md @@ -198,7 +198,9 @@ for name, rate in zip(("Relaxation", "Dephasing"), fit.best_parameters, strict=T With two free parameters, YAQS uses the derivative-free CMA-ES optimizer. `max_iter=40` limits its generations, and `seed` fixes the optimizer's random search. This seed is separate from `AnalogSimParams.random_seed`, which controls -stochastic simulation. Parallel execution remains enabled by default. +stochastic simulation. `NoiseCharacterizer` defaults to in-process execution; +set `parallel=True` to parallelize trajectories for vector or MPS forward +models. For observations $z_{o,t}$, the fitted objective is diff --git a/docs/examples/equivalence_checking.md b/docs/examples/equivalence_checking.md index 65c9e7efe..61056b661 100644 --- a/docs/examples/equivalence_checking.md +++ b/docs/examples/equivalence_checking.md @@ -287,7 +287,7 @@ every sampled overlap is zero does not establish zero uncertainty. Keep `representation="auto"` for automatic selection, or choose `"matrix"` or `"mpo"` explicitly. Dense matrix storage grows as $4^n$; MPO cost depends on the operator's bond dimensions and can also grow rapidly. The MPO method is -described in {cite:p}`sander2025_EquivalenceChecking`. +described in {footcite:p}`sander2025_EquivalenceChecking`. The constructor's `fidelity` sets the decision threshold, while `threshold` sets the MPO singular-value cutoff. These control different errors. For an @@ -324,3 +324,6 @@ Parallel execution is enabled by default. `max_workers` caps concurrency, and `random_seed` makes sampled trajectories reproducible across worker scheduling. See {class}`~mqt.yaqs.EquivalenceChecker` for the full settings and returned fields. + +```{footbibliography} +``` diff --git a/docs/examples/representation_comparison.md b/docs/examples/representation_comparison.md index 2a7ab389d..ff7b49931 100644 --- a/docs/examples/representation_comparison.md +++ b/docs/examples/representation_comparison.md @@ -273,6 +273,7 @@ solver-specific controls. | Static-Hamiltonian analog evolution | MPS, vector, and density matrix; noise must meet the restrictions in {doc}`realistic_noise_models`. | | Circuits and analog-digital programs | MPS. | | Mixed initial state | Density matrix. | +| Bitstring-probability observables | MPS with qubits at every site. | | Entropy and Schmidt-spectrum observables | MPS; noisy results describe pure trajectories, not the spectrum of a mixed density matrix. | | Piecewise Hamiltonian | MPS with TDVP; see {doc}`hamiltonians`. | | Unitary `list[State]` ensemble | MPS; see {doc}`ensemble_evolution`. | diff --git a/docs/examples/simulation_parameters.md b/docs/examples/simulation_parameters.md index d97998cf4..ae2bd7fb1 100644 --- a/docs/examples/simulation_parameters.md +++ b/docs/examples/simulation_parameters.md @@ -143,8 +143,9 @@ to $\sigma^\alpha$, rather than spin operators $S^\alpha=\sigma^\alpha/2$. Two-site local operators support adjacent sites and the periodic end-to-end bond on qubit chains. Matrix factors follow the supplied site order. Bitstring -probability requests cannot share an observable list with ordinary operators or -entanglement diagnostics; shot counts can accompany ordinary observables. +probabilities require an all-qubit MPS and cannot share an observable list with +ordinary operators or entanglement diagnostics. Shot counts can accompany +ordinary observables. For MPS entanglement across the bond between sites 1 and 2, request the adjacent pair: diff --git a/docs/examples/trapped_ion.md b/docs/examples/trapped_ion.md index ab429da6a..caa1d6da2 100644 --- a/docs/examples/trapped_ion.md +++ b/docs/examples/trapped_ion.md @@ -292,8 +292,12 @@ Check grid spacing and boundaries before interpreting a quantitative result. The kinetic operator uses zero exterior boundary values, and a heated packet can reach the edges. Refine the grid and enlarge its range separately. The continuum Gaussian is also only an approximate ground state of the discrete Hamiltonian. -For dimensional inputs, use compatible units and supply a Hamiltonian divided by -$\hbar$ if your time unit requires it: YAQS evolves with $\exp(-iH\,dt)$. +For dimensional inputs, pass `hbar` to `MPO.trapped_ion` in units consistent +with the masses, positions, and angular frequency. The kinetic term depends on +$\hbar^2$. For time in seconds, divide one MPO core by $\hbar$ with +`mpo.tensors[0] /= hbar` before wrapping it with `Hamiltonian.from_mpo`. This +converts the energy operator to $H/\hbar$ for YAQS's evolution convention +$\exp(-iH\,dt)$. A slower or smoother transport protocol can reduce coherent residual motion. The kick model isolates heating from random impulses. Other noise processes require From 763480ed6de0d4ae1717b4e1da25502201b12682 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:57:55 +0200 Subject: [PATCH 22/30] fixed broken docs references in docstrings --- src/mqt/yaqs/analog/mcwf.py | 2 +- .../memory/backends/surrogates/model.py | 6 ++++-- .../memory/backends/surrogates/workflow.py | 6 ++++-- .../memory/backends/tomography/process_tensors.py | 10 ++++++---- src/mqt/yaqs/characterization/memory/shared/metrics.py | 4 ++-- src/mqt/yaqs/characterization/memory/shared/utils.py | 2 +- src/mqt/yaqs/core/data_structures/hamiltonian.py | 2 +- src/mqt/yaqs/core/data_structures/hamiltonian_utils.py | 4 ++-- src/mqt/yaqs/core/data_structures/mpo.py | 2 +- src/mqt/yaqs/core/data_structures/state.py | 4 ++-- src/mqt/yaqs/core/libraries/circuit_library.py | 3 +-- src/mqt/yaqs/core/libraries/noise_library.py | 7 ++++--- src/mqt/yaqs/core/linalg/__init__.py | 3 ++- src/mqt/yaqs/core/methods/bug.py | 4 ++-- src/mqt/yaqs/digital/utils/contraction_utils.py | 4 ++-- 15 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/mqt/yaqs/analog/mcwf.py b/src/mqt/yaqs/analog/mcwf.py index acd133a30..927285b19 100644 --- a/src/mqt/yaqs/analog/mcwf.py +++ b/src/mqt/yaqs/analog/mcwf.py @@ -79,7 +79,7 @@ def preprocess_mcwf( ) -> MCWFContext: """Pre-compute dense operators and initial state for MCWF simulation. - Called once per :meth:`Simulator.run` before trajectory workers start. + Called once per :meth:`~mqt.yaqs.Simulator.run` before trajectory workers start. Args: psi_initial: Dense state vector (unit norm applied here). diff --git a/src/mqt/yaqs/characterization/memory/backends/surrogates/model.py b/src/mqt/yaqs/characterization/memory/backends/surrogates/model.py index 7150f871a..9db8614dd 100644 --- a/src/mqt/yaqs/characterization/memory/backends/surrogates/model.py +++ b/src/mqt/yaqs/characterization/memory/backends/surrogates/model.py @@ -209,7 +209,7 @@ def predict_final_state_batch( the batch size of ``e_features``. e_features: Per-step features of shape ``(B, T, d_e)``. restore_training: If ``False``, do not restore ``.train()`` after inference (for callers that - run many batched predictions in a loop, e.g. :meth:`entropy`). + run many batched predictions in a loop). Returns: Tensor of shape ``(B, d_rho)``. @@ -253,7 +253,9 @@ def _num_interventions_for_probe(self) -> int: return int(self.num_interventions) def evaluate_probes(self, probe_set: ProbeSet, *, initial_rho: np.ndarray | None = None) -> np.ndarray: - """Evaluate split-cut probe responses for :func:`run_memory_characterization`. + """Evaluate split-cut probe responses. + + Used by :func:`~mqt.yaqs.characterization.memory.operational_memory.run.run_memory_characterization`. Args: probe_set: Sampled split-cut probes. diff --git a/src/mqt/yaqs/characterization/memory/backends/surrogates/workflow.py b/src/mqt/yaqs/characterization/memory/backends/surrogates/workflow.py index c66c121db..7d693184b 100644 --- a/src/mqt/yaqs/characterization/memory/backends/surrogates/workflow.py +++ b/src/mqt/yaqs/characterization/memory/backends/surrogates/workflow.py @@ -167,7 +167,8 @@ def build_training_dataset( show_progress: Whether to show progress bars. timesteps: Optional process-tensor schedule evolution durations (defaults to ``[sim_params.dt] * (num_interventions + 1)``). - init_mode: Initial-state sampling mode (see :func:`sample_initial_psi`). + init_mode: Initial-state sampling mode (see + :func:`~mqt.yaqs.characterization.memory.backends.surrogates.utils.sample_initial_psi`). solver: Stochastic solver (``"MCWF"`` or ``"TJM"``); defaults to ``"MCWF"``. intervention_style: Training intervention style (``"haar"``, ``"clifford"``, or ``"measure_prepare"``). @@ -290,7 +291,8 @@ def train_surrogate_model( :func:`build_training_dataset`; defaults to ``"MCWF"``. intervention_style: Training intervention style passed to :func:`build_training_dataset`. model_kwargs: Optional keyword arguments forwarded to :class:`ProcessTensorSurrogate`. - train_kwargs: Optional keyword arguments forwarded to :meth:`ProcessTensorSurrogate.fit`. + train_kwargs: Optional keyword arguments forwarded to + :meth:`~mqt.yaqs.characterization.memory.backends.surrogates.model.ProcessTensorSurrogate.fit`. Returns: Trained :class:`ProcessTensorSurrogate`. diff --git a/src/mqt/yaqs/characterization/memory/backends/tomography/process_tensors.py b/src/mqt/yaqs/characterization/memory/backends/tomography/process_tensors.py index 770b80777..fc7b05a6e 100644 --- a/src/mqt/yaqs/characterization/memory/backends/tomography/process_tensors.py +++ b/src/mqt/yaqs/characterization/memory/backends/tomography/process_tensors.py @@ -200,7 +200,7 @@ def validate_initial_rho( def convert_probe_callable( step: AnyInterventionStep, ) -> Callable[[NDArray[np.complex128]], NDArray[np.complex128]]: - """Convert a probe-grid step to a CP map callable for :meth:`~SupportsPredict.predict`. + """Convert a probe-grid step to a CP map callable for process-tensor prediction. Args: step: Structured dict step or measure/prepare ket pair. @@ -220,13 +220,14 @@ def unitary_map(rho: NDArray[np.complex128]) -> NDArray[np.complex128]: def evaluate_probes(process_tensor: SupportsPredict, probe_set: ProbeSet) -> np.ndarray: - """Evaluate split-cut probe Pauli responses via process-tensor :meth:`predict`. + """Evaluate split-cut probe Pauli responses through process-tensor prediction. Shared by dense and MPO process tensors for operational-memory V-matrix assembly. Does not densify MPO tensors. Args: - process_tensor: Backend implementing :meth:`~SupportsPredict.predict`. + process_tensor: Backend implementing + :meth:`~mqt.yaqs.characterization.memory.backends.tomography.process_tensors.SupportsPredict.predict`. probe_set: Sampled split-cut probes. Returns: @@ -1043,7 +1044,8 @@ def compute_temporal_entropy( def evaluate_probes(self, probe_set: ProbeSet) -> np.ndarray: """Evaluate split-cut probe Pauli responses for V-matrix assembly. - Uses native MPO :meth:`predict` (does not densify the process tensor). + Uses :meth:`~mqt.yaqs.characterization.memory.backends.tomography.process_tensors.MPOProcessTensor.predict` + without densifying the process tensor. Args: probe_set: Sampled split-cut probes. diff --git a/src/mqt/yaqs/characterization/memory/shared/metrics.py b/src/mqt/yaqs/characterization/memory/shared/metrics.py index b5d698ed8..0103f2622 100644 --- a/src/mqt/yaqs/characterization/memory/shared/metrics.py +++ b/src/mqt/yaqs/characterization/memory/shared/metrics.py @@ -59,7 +59,7 @@ def compute_rel_fro_error(a_mat: NDArray[np.complex128], b_mat: NDArray[np.compl b_mat: Reference matrix. Returns: - Relative Frobenius error: ||A-B||_F / max(||B||_F, eps). + float: Relative Frobenius error, ||A-B||_F / max(||B||_F, eps). """ a, b = _validate_square_matrix_pair(a_mat, b_mat, name_a="a_mat", name_b="b_mat") num = np.linalg.norm(a - b, "fro") @@ -75,7 +75,7 @@ def compute_trace_distance(rho: NDArray[np.complex128], sigma: NDArray[np.comple sigma: Density matrix. Returns: - Trace distance: 0.5 * ||rho - sigma||_1. + float: Trace distance, 0.5 * ||rho - sigma||_1. """ rho_h, sigma_h = _validate_square_matrix_pair(rho, sigma, name_a="rho", name_b="sigma") diff_mat = rho_h - sigma_h diff --git a/src/mqt/yaqs/characterization/memory/shared/utils.py b/src/mqt/yaqs/characterization/memory/shared/utils.py index 9717cec20..6fb3f4540 100644 --- a/src/mqt/yaqs/characterization/memory/shared/utils.py +++ b/src/mqt/yaqs/characterization/memory/shared/utils.py @@ -120,7 +120,7 @@ def _dense_state_to_mps(psi: NDArray[np.complex128], *, length: int) -> MPS: """Convert a dense site-0-LSB qubit state to an MPS to numerical precision. Args: - psi: Dense state vector in the same order as :meth:`MPS.to_vec`. + psi: Dense state vector in the same order as :meth:`~mqt.yaqs.MPS.to_vec`. length: Number of qubits in the state. Returns: diff --git a/src/mqt/yaqs/core/data_structures/hamiltonian.py b/src/mqt/yaqs/core/data_structures/hamiltonian.py index 0081c5052..2a61734cf 100644 --- a/src/mqt/yaqs/core/data_structures/hamiltonian.py +++ b/src/mqt/yaqs/core/data_structures/hamiltonian.py @@ -512,7 +512,7 @@ def _warn_large_hilbert_dim(dim: int, *, action: str) -> None: def ensure_mpo(self) -> Hamiltonian: """Materialize and cache an MPO form (used by TJM / ``State.representation='mps'``). - Dense and sparse sources are converted via :meth:`MPO.from_matrix` + Dense and sparse sources are converted via :meth:`~mqt.yaqs.MPO.from_matrix` without changing the public site order. Sparse input is densified only when this path is requested. Large Hilbert-space conversions emit a ``RuntimeWarning`` matching the ``preprocess_mcwf`` threshold. diff --git a/src/mqt/yaqs/core/data_structures/hamiltonian_utils.py b/src/mqt/yaqs/core/data_structures/hamiltonian_utils.py index e95c71326..d5352a5ae 100644 --- a/src/mqt/yaqs/core/data_structures/hamiltonian_utils.py +++ b/src/mqt/yaqs/core/data_structures/hamiltonian_utils.py @@ -26,7 +26,7 @@ def sparse_to_csr(matrix: scipy.sparse.spmatrix) -> scipy.sparse.csr_matrix: def attach_mpo(wrapped: Hamiltonian, mpo: MPO) -> None: - """Initialize ``wrapped`` from an existing MPO (factory helper for :meth:`Hamiltonian.from_mpo`).""" + """Initialize ``wrapped`` from an existing MPO (helper for :meth:`~mqt.yaqs.Hamiltonian.from_mpo`).""" wrapped.length = mpo.length wrapped.physical_dimension = mpo.physical_dimension # Private fields: wrapped is a fresh Hamiltonian from __new__; attach_mpo is the sole initializer. @@ -42,7 +42,7 @@ def attach_piecewise( pieces: tuple[tuple[Hamiltonian, float], ...], length: int, ) -> None: - """Initialize ``wrapped`` from static Hamiltonian pieces (factory helper for :meth:`Hamiltonian.piecewise`).""" + """Initialize ``wrapped`` from static Hamiltonian pieces (helper for :meth:`~mqt.yaqs.Hamiltonian.piecewise`).""" wrapped.length = length wrapped.physical_dimension = pieces[0][0].physical_dimension wrapped._tensors = None # ruff:ignore[private-member-access] diff --git a/src/mqt/yaqs/core/data_structures/mpo.py b/src/mqt/yaqs/core/data_structures/mpo.py index 8d38a3c56..d2db6719a 100644 --- a/src/mqt/yaqs/core/data_structures/mpo.py +++ b/src/mqt/yaqs/core/data_structures/mpo.py @@ -1915,7 +1915,7 @@ def to_matrix(self) -> NDArray[np.complex128]: :meth:`to_sparse_matrix` use the same order. Returns: - Dense operator matrix acting on vectors from :meth:`MPS.to_vec`. + Dense operator matrix acting on vectors from :meth:`~mqt.yaqs.MPS.to_vec`. """ mat = self.tensors[-1] for tensor in reversed(self.tensors[:-1]): diff --git a/src/mqt/yaqs/core/data_structures/state.py b/src/mqt/yaqs/core/data_structures/state.py index 5644695da..772804fb5 100644 --- a/src/mqt/yaqs/core/data_structures/state.py +++ b/src/mqt/yaqs/core/data_structures/state.py @@ -92,7 +92,7 @@ class State: """Initial quantum state for :meth:`~mqt.yaqs.Simulator.run`. Specify *what* to simulate (length, preset, optional raw data) and *how* to represent it - during evolution (:attr:`representation`). Materialization happens at construction; + during evolution (``representation``). Materialization happens at construction; pass the ``State`` to :meth:`~mqt.yaqs.Simulator.run` (including in parameter loops). - **Presets** — ``State(L, initial="zeros")``; default ``representation="mps"`` (TJM). @@ -330,7 +330,7 @@ def _dense_vector_from_preset(self) -> NDArray[np.complex128]: @property def mps(self) -> MPS: - """MPS when :attr:`representation` is ``"mps"``. + """MPS when ``representation`` is ``"mps"``. Raises: RuntimeError: If the state is not encoded as ``"mps"``. diff --git a/src/mqt/yaqs/core/libraries/circuit_library.py b/src/mqt/yaqs/core/libraries/circuit_library.py index bb403e1c7..7b4d11865 100644 --- a/src/mqt/yaqs/core/libraries/circuit_library.py +++ b/src/mqt/yaqs/core/libraries/circuit_library.py @@ -39,8 +39,7 @@ def create_ising_circuit( g (float): Transverse field strength. dt (float): Time step for the simulation. timesteps (int): Number of time steps to simulate. - periodic (bool, optional): If True, add a long-range gate between qubits 0 and L-1. - Defaults to False. + periodic: If True, add a long-range gate between qubits 0 and L-1. Defaults to False. Returns: QuantumCircuit: A quantum circuit representing the Ising model evolution. diff --git a/src/mqt/yaqs/core/libraries/noise_library.py b/src/mqt/yaqs/core/libraries/noise_library.py index 8e68d6bdc..b010359aa 100644 --- a/src/mqt/yaqs/core/libraries/noise_library.py +++ b/src/mqt/yaqs/core/libraries/noise_library.py @@ -209,15 +209,16 @@ class NoiseLibrary: lowering_two: Two-site lowering noise (11 --> 00). crosstalk_zz: Cross talk between neighboring sites along the z-axis. crosstalk_xx: Cross talk between neighboring sites along the x-axis. - crosstalk_y: Cross talk between neighboring sites along the y-axis. + crosstalk_yy: Cross talk between neighboring sites along the y-axis. crosstalk_xy: Cross talk between neighboring sites with X x Y. crosstalk_yx: Cross talk between neighboring sites with Y x X. crosstalk_zy: Cross talk between neighboring sites with Z x Y. crosstalk_zx: Cross talk between neighboring sites with Z x X. crosstalk_yz: Cross talk between neighboring sites with Y x Z. crosstalk_xz: Cross talk between neighboring sites with X x Z. - Note: Long-range crosstalk is handled by NoiseModel by attaching per-site - factors for non-adjacent pairs based on the process name (e.g., 'crosstalk_xy'). + + ``NoiseModel`` handles long-range crosstalk with per-site factors for + non-adjacent pairs, inferred from the process name (e.g., ``crosstalk_xy``). """ # Canonical names diff --git a/src/mqt/yaqs/core/linalg/__init__.py b/src/mqt/yaqs/core/linalg/__init__.py index 346c17060..48198ecf7 100644 --- a/src/mqt/yaqs/core/linalg/__init__.py +++ b/src/mqt/yaqs/core/linalg/__init__.py @@ -8,7 +8,8 @@ """SciPy-style dense linear algebra with BLAS-thread-safe defaults. This package mirrors :mod:`scipy.linalg` for the subset of operations YAQS uses -internally; submodules group related helpers (e.g. :mod:`.expm`, :mod:`.svd`). +internally; submodules group related helpers (e.g. :mod:`~mqt.yaqs.core.linalg.expm`, +:mod:`~mqt.yaqs.core.linalg.svd`). """ from __future__ import annotations diff --git a/src/mqt/yaqs/core/methods/bug.py b/src/mqt/yaqs/core/methods/bug.py index 74197cefa..0c23e2b46 100644 --- a/src/mqt/yaqs/core/methods/bug.py +++ b/src/mqt/yaqs/core/methods/bug.py @@ -43,8 +43,8 @@ def prepare_canonical_site_tensors( mpo: The MPO. Returns: - canon_tensors: The list of the canonical site tensors. - left_blocks: The list of the left environments. + Tuple ``(canon_tensors, left_blocks)`` containing the canonical site tensors + and left MPO environments. """ canon_tensors = copy(state.tensors) left_end_dimension = state.tensors[0].shape[1] diff --git a/src/mqt/yaqs/digital/utils/contraction_utils.py b/src/mqt/yaqs/digital/utils/contraction_utils.py index cd00e50cc..bfc606eda 100644 --- a/src/mqt/yaqs/digital/utils/contraction_utils.py +++ b/src/mqt/yaqs/digital/utils/contraction_utils.py @@ -56,7 +56,7 @@ def apply_gate( theta (NDArray[np.complex128]): The local tensor to update. site0 (int): The first qubit (site) index. site1 (int): The second qubit (site) index. - conjugate (bool, optional): Whether to apply the conjugated version of the gate tensor. Defaults to False. + conjugate: Whether to apply the conjugated version of the gate tensor. Defaults to False. Returns: NDArray[np.complex128]: The updated local tensor after applying the gate. @@ -140,7 +140,7 @@ def apply_temporal_zone( theta (NDArray[np.complex128]): The local tensor to update. dag (DAGCircuit): The DAGCircuit from which to extract the temporal zone. qubits (list[int]): The qubit indices on which to apply the temporal zone (typically two neighboring qubits). - conjugate (bool, optional): Whether to apply the gates in conjugated form. Defaults to False. + conjugate: Whether to apply the gates in conjugated form. Defaults to False. Returns: NDArray[np.complex128]: The updated tensor after applying the temporal zone. From 49d5f8c166bc31d64848123c6925abc115d5408a Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:25:51 +0200 Subject: [PATCH 23/30] added tests to guarantee docs tests run properly --- .github/workflows/ci.yml | 15 +++ .readthedocs.yaml | 1 + docs/_ext/yaqs_api.py | 221 +++++++++++++++++++++++++++++++++++++++ docs/conf.py | 33 +++++- noxfile.py | 27 ++++- tests/docs/test_build.py | 184 ++++++++++++++++++++++++++++++-- 6 files changed, 467 insertions(+), 14 deletions(-) create mode 100644 docs/_ext/yaqs_api.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdeec134a..900b5cd2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,20 @@ jobs: permissions: contents: read + docs-check: + name: 📚 Documentation + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + - name: Check documentation + run: uvx nox --non-interactive -s docs-check + build-sdist: name: 🚀 CD needs: change-detection @@ -90,6 +104,7 @@ jobs: - python-tests - python-coverage - python-linter + - docs-check - build-sdist - build-wheel runs-on: ubuntu-slim diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 8c8f66239..2e28b0866 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -2,6 +2,7 @@ version: 2 sphinx: configuration: docs/conf.py + fail_on_warning: true build: os: ubuntu-24.04 diff --git a/docs/_ext/yaqs_api.py b/docs/_ext/yaqs_api.py new file mode 100644 index 000000000..98af5e0ce --- /dev/null +++ b/docs/_ext/yaqs_api.py @@ -0,0 +1,221 @@ +# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Keep API re-exports and type references linked to their defining objects.""" + +from __future__ import annotations + +import builtins +import shutil +from html import escape +from pathlib import Path +from typing import TYPE_CHECKING + +from docutils import nodes +from sphinx import addnodes +from sphinx.domains.python import ObjectEntry, PythonDomain + +if TYPE_CHECKING: + from collections.abc import Iterator + + from autoapi._objects import PythonObject + from sphinx.application import Sphinx + from sphinx_markdown_builder.translator import MarkdownTranslator + + +_EXTERNAL_ALIASES = { + "NDArray": "numpy.typing.NDArray", + "ArrayLike": "numpy.typing.ArrayLike", + "QuantumCircuit": "qiskit.circuit.QuantumCircuit", + "qiskit.QuantumCircuit": "qiskit.circuit.QuantumCircuit", + "DAGCircuit": "qiskit.dagcircuit.DAGCircuit", + "DAGOpNode": "qiskit.dagcircuit.DAGOpNode", + "Parameter": "qiskit.circuit.Parameter", + "scipy.linalg.LinAlgError": "numpy.linalg.LinAlgError", +} + + +class _APIPythonDomain(PythonDomain): + """Publish public import aliases alongside their canonical API entries.""" + + def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: + """Include import aliases, even when a module has the same name. + + Yields: + Python inventory entries. + """ + yield from super().get_objects() + for name, entry in self.data.get("yaqs_public_aliases", {}).items(): + if name not in self.objects or self.objects[name].objtype != entry.objtype: + yield name, name, entry.objtype, entry.docname, entry.node_id, -1 + + +def _canonical_name(objects: dict[str, PythonObject], name: str) -> str: + """Follow a re-export, including a method or attribute below that export. + + Returns: + The object's name at its defining module. + """ + visited = set() + while True: + parts = name.split(".") + for end in range(len(parts), 0, -1): + obj = objects.get(".".join(parts[:end])) + if obj is not None and obj.imported: + if end < len(parts) and obj.type not in {"class", "exception", "module", "package"}: + continue + if obj.id in visited: + return name + visited.add(obj.id) + original = obj.obj["original_path"] + suffix = ".".join(parts[end:]) + name = original + (f".{suffix}" if suffix else "") + break + else: + return name + + +def _public_imports( + app: Sphinx, + what: str, + name: str, + obj: PythonObject, + skip: bool, # ruff: ignore[boolean-type-hint-positional-argument] - Sphinx calls event handlers positionally. + options: list[str], +) -> None: + """List explicit public re-exports without repeating their documentation.""" + del app, name, options + if skip or what not in {"module", "package"}: + return + exports = obj.obj.get("all") or [] + links = [ + f"* :py:obj:`{child.name} <{child.obj['original_path']}>`" + for child in obj.children + if child.imported and child.name in exports + ] + if links: + obj.docstring += "\n\n.. rubric:: Public imports\n\n" + "\n".join(links) + "\n" + + +def _resolve_api_references(app: Sphinx, doctree: nodes.document) -> None: + """Qualify local aliases and use inventory roles for external types.""" + objects = getattr(app.env, "autoapi_all_objects", {}) + inventory = getattr(app.env, "intersphinx_inventory", {}) + for node in doctree.findall(addnodes.pending_xref): + if node.get("refdomain") != "py": + continue + target = node["reftarget"] + if node.get("reftype") in {"class", "obj"} and isinstance(getattr(builtins, target, None), type): + # Avoid resolving builtin ``type`` or ``float`` to a class member. + node["reftarget"] = f"python:{target}" + continue + module = node.get("py:module", "") + classname = node.get("py:class", "") + candidates = [target, f"{module}.{classname}.{target}", f"{module}.{target}"] + if target.startswith("."): + candidates.insert(0, module + target) + # Resolve within the declared scope before searching globally. The + # imported object remains in AutoAPI's model even when it is not rendered. + for candidate in candidates: + if candidate in objects: + target = _canonical_name(objects, candidate) if node.get("reftype") != "mod" else candidate + break + else: + if "." not in target: + matches = { + name + for name, obj in objects.items() + if name.endswith(f".{target}") and obj.display and not obj.imported + } + if len(matches) == 1: + target = matches.pop() + + target = _EXTERNAL_ALIASES.get(target, target) + if target.startswith("np."): + target = "numpy." + target[3:] + obj = objects.get(target) + if ( + obj is not None and not obj.display and not obj.imported + ) or target == "multiprocessing.context.BaseContext": + # Keep known internal types readable without linking to deliberately + # omitted API entries. Python has no inventory entry for BaseContext. + # Unknown YAQS names still pass through to Sphinx's strict check. + node.replace_self(nodes.literal("", node.astext())) + continue + node["reftarget"] = target + if node.get("reftype") == "class" and any( + target in inventory.get(role, {}) for role in ("py:data", "py:attribute", "py:type") + ): + # NumPy documents NDArray as data and scalar dtypes as attributes. + # Generic object roles retain the exact type and use those entries. + node["reftype"] = "obj" + + +def _register_public_aliases(app: Sphinx, env: object) -> None: + """Keep public re-export names available to downstream intersphinx users.""" + del env + objects = getattr(app.env, "autoapi_all_objects", {}) + domain = app.env.domains["py"] + canonical_entries = dict(domain.objects) + aliases = {} + for name, obj in objects.items(): + if not obj.imported or obj.obj.get("hide"): + continue + canonical = _canonical_name(objects, name) + for target, entry in canonical_entries.items(): + if target == canonical or target.startswith(f"{canonical}."): + alias = name + target[len(canonical) :] + aliases[alias] = ObjectEntry(entry.docname, entry.node_id, entry.objtype, aliased=True) + domain.data["yaqs_public_aliases"] = aliases + + +def _markdown_images(app: Sphinx, doctree: nodes.document, docname: str) -> None: + """Keep local image links valid when sphinx-llm merges Markdown outputs.""" + if app.builder.name not in {"llms-markdown", "markdown"}: + return + output = Path(app.outdir) + if app.tags.has("sphinx_llm_markdown"): + output = output.parent + for node in doctree.findall(nodes.image): + image = app.env.images.get(node["uri"]) + if image is None: + continue + destination = output / "_images" / image[1] + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(Path(app.srcdir) / node["uri"], destination) + node["uri"] = "../" * docname.count("/") + f"_images/{image[1]}" + + +def _visit_markdown_abbreviation(translator: MarkdownTranslator, node: nodes.abbreviation) -> None: + """Keep abbreviation explanations and signature separators in Markdown.""" + translator.add(f'') + + +def _depart_markdown_abbreviation(translator: MarkdownTranslator, node: nodes.abbreviation) -> None: + """Close an abbreviation after the translator has rendered its text.""" + del node + translator.add("") + + +def setup(app: Sphinx) -> dict[str, bool]: + """Install API presentation hooks. + + Returns: + Parallel build support declarations. + """ + if "autoapi-skip-member" in app.events.events: + app.connect("autoapi-skip-member", _public_imports) + app.add_domain(_APIPythonDomain, override=True) + app.add_node( + nodes.abbreviation, + override=True, + markdown=(_visit_markdown_abbreviation, _depart_markdown_abbreviation), + ) + app.connect("doctree-read", _resolve_api_references) + app.connect("env-updated", _register_public_aliases) + app.connect("doctree-resolved", _markdown_images) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/conf.py b/docs/conf.py index 1b8ce0a9c..014288c4a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,6 +10,7 @@ from __future__ import annotations import os +import sys from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING @@ -23,6 +24,7 @@ from pybtex.richtext import HRef ROOT = Path(__file__).parent.parent.resolve() +sys.path.insert(0, str(Path(__file__).parent / "_ext")) # Limit docs kernels and child builds unless the runner supplies a budget. for name in ( @@ -33,7 +35,8 @@ "OPENBLAS_NUM_THREADS", ): os.environ.setdefault(name, "1") -os.environ.setdefault("YAQS_MAX_WORKERS", "2") +# Automatic worker selection reserves one CPU, so this hint permits two workers. +os.environ.setdefault("YAQS_MAX_WORKERS", "3") # Keep matplotlib/font cache writable and local during docs builds. os.environ.setdefault("MPLCONFIGDIR", str(ROOT / "docs" / "_build" / ".mplconfig")) @@ -70,9 +73,11 @@ "sphinx.ext.viewcode", "sphinxcontrib.bibtex", "sphinxext.opengraph", + "yaqs_api", ] source_suffix = [".rst", ".md"] +nitpicky = True exclude_patterns = [ "_build", @@ -90,7 +95,9 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable/", None), - "qiskit": ("https://docs.quantum.ibm.com/api/qiskit", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), + "torch": ("https://docs.pytorch.org/docs/stable/", None), + "qiskit": ("https://quantum.cloud.ibm.com/docs/api/qiskit", None), "mqt": ("https://mqt.readthedocs.io/en/stable", None), "core": ("https://mqt.readthedocs.io/projects/core/en/stable", None), "ddsim": ("https://mqt.readthedocs.io/projects/ddsim/en/stable", None), @@ -116,6 +123,21 @@ nb_execution_mode = "cache" nb_execution_raise_on_error = True nb_execution_cache_path = str(ROOT / "docs" / "_build" / ".jupyter_cache") +# MyST-NB does not know sphinx-llm's builder name. Preserve figures instead of +# selecting only their text representation in the generated Markdown. +nb_mime_priority_overrides = [ + ("llms-markdown", mime, priority) + for priority, mime in enumerate(( + "image/svg+xml", + "image/png", + "image/jpeg", + "image/gif", + "text/markdown", + "text/latex", + "text/html", + "text/plain", + )) +] # Reuse HTML doctrees and notebook outputs when generating the Markdown files. llms_txt_build_parallel = False @@ -155,17 +177,20 @@ def format_url(self, _e: Entry) -> HRef: # ruff:ignore[no-self-use] ] autoapi_options = [ "members", - "imported-members", "show-inheritance", "special-members", "undoc-members", ] -autoapi_keep_files = True +# Do not carry generated pages for removed modules into the next build. +autoapi_keep_files = False add_module_names = False toc_object_entries_show_parents = "hide" python_use_unqualified_type_names = True napoleon_google_docstring = True napoleon_numpy_docstring = False +# AutoAPI already indexes the real attributes. Keep Google-style attribute +# descriptions as fields, without creating a second target for each attribute. +napoleon_use_ivar = True # -- Options for HTML output ------------------------------------------------- diff --git a/noxfile.py b/noxfile.py index a61dc8695..06beb198e 100755 --- a/noxfile.py +++ b/noxfile.py @@ -253,6 +253,8 @@ def docs(session: nox.Session) -> None: shared_args = [ "-n", # nitpicky mode "-T", # full tracebacks + "-W", # fail on warnings + "--keep-going", f"-b={args.builder}", "docs", f"docs/_build/{args.builder}", @@ -262,7 +264,30 @@ def docs(session: nox.Session) -> None: session.run( "sphinx-autobuild" if serve else "sphinx-build", *shared_args, - env={**_CAPPED_NUMERICAL_THREADS, "YAQS_MAX_WORKERS": "2"}, + env={**_CAPPED_NUMERICAL_THREADS, "YAQS_MAX_WORKERS": "3"}, + ) + + +@nox.session(name="docs-check", python="3.14", reuse_venv=True) +def docs_check(session: nox.Session) -> None: + """Test the build settings and check references without running guide notebooks.""" + session.install("--group", "docs", "--group", "test", "--torch-backend", "cpu", "--exact", "-e", ".[qasm3,torch]") + session.run("pytest", "-n", "0", "tests/docs", env=_CAPPED_NUMERICAL_THREADS) + session.run( + "sphinx-build", + "-E", + "-a", + "-n", + "-T", + "-W", + "--keep-going", + "-D", + "nb_execution_mode=off", + "-D", + "llms_txt_enabled=0", + "docs", + "docs/_build/check", + env={**_CAPPED_NUMERICAL_THREADS, "YAQS_MAX_WORKERS": "3"}, ) diff --git a/tests/docs/test_build.py b/tests/docs/test_build.py index 4ec82195e..6bc4f82a2 100644 --- a/tests/docs/test_build.py +++ b/tests/docs/test_build.py @@ -11,32 +11,49 @@ import json import os +import re +import shutil import subprocess import sys +import zlib from pathlib import Path import pytest -def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: - """Both documentation formats retain outputs from one capped notebook run.""" - pytest.importorskip("sphinx") - pytest.importorskip("myst_nb") - pytest.importorskip("sphinx_llm.txt") - pytest.importorskip("pybtex") +def _write_configuration(source: Path, extensions: list[str], extra: str = "") -> None: + """Use the real build settings with a small, self-contained documentation tree. - source = tmp_path / "docs" - source.mkdir() + Args: + source: Documentation source directory. + extensions: Extensions needed by this test. + extra: Additional configuration settings. + """ configuration = Path(__file__).parents[2] / "docs" / "conf.py" + shutil.copytree(configuration.parent / "_ext", source / "_ext") (source / "conf.py").write_text( configuration.read_text() - + '\nextensions = ["myst_nb", "sphinx_llm.txt"]\n' + + f"\nextensions = {extensions!r}\n" + 'html_theme = "basic"\n' + "html_theme_options = {}\n" + "html_static_path = []\n" + "html_css_files = []\n" + "templates_path = []\n" + + "intersphinx_mapping = {}\n" + + extra ) + + +def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: + """Both documentation formats retain outputs from one capped notebook run.""" + pytest.importorskip("sphinx") + pytest.importorskip("myst_nb") + pytest.importorskip("sphinx_llm.txt") + pytest.importorskip("pybtex") + + source = tmp_path / "docs" + source.mkdir() + _write_configuration(source, ["myst_nb", "sphinx_llm.txt", "yaqs_api"]) records = tmp_path / "executions.jsonl" (source / "index.md").write_text( "---\nfile_format: mystnb\nkernelspec:\n name: python3\nlanguage_info:\n name: python\n---\n\n" @@ -49,6 +66,7 @@ def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: "import numpy as np\n" "from threadpoolctl import threadpool_info\n" "from mqt.yaqs import Simulator\n\n" + "from IPython.display import SVG, display\n\n" "np.eye(4) @ np.eye(4)\n" "record = {\n" ' "thread_limits": {name: os.environ[name] for name in (\n' @@ -62,6 +80,8 @@ def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: f"with Path({str(records)!r}).open('a') as stream:\n" " stream.write(json.dumps(record) + '\\n')\n" 'print("NOTEBOOK_RESULT_" + str(6 * 7))\n' + 'display(SVG(\'' + '\'))\n' "```\n" ) output = tmp_path / "_build" / "html" @@ -88,6 +108,10 @@ def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: ) assert completed.returncode == 0, completed.stdout + completed.stderr + assert "WARNING:" not in completed.stdout + completed.stderr + child_log = re.search(r"Subprocess output available at: (.+)", completed.stdout) + assert child_log is not None, completed.stdout + assert "WARNING:" not in Path(child_log[1].strip()).read_text(encoding="utf-8") runs = [json.loads(line) for line in records.read_text().splitlines()] assert len(runs) == 1 assert all(limit == "1" for limit in runs[0]["thread_limits"].values()) @@ -96,3 +120,145 @@ def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: assert all(threads == 1 for threads in runs[0]["blas_threads"]) for artifact in ("index.html", "index.html.md", "llms-full.txt"): assert "NOTEBOOK_RESULT_42" in (output / artifact).read_text() + markdown = (output / "index.html.md").read_text() + figures = re.findall(r"!\[[^\]]*\]\(([^)]+\.svg)\)", markdown) + assert len(figures) == 1, markdown + assert (output / figures[0]).is_file() + assert figures[0] in (output / "index.html").read_text() + assert "image/svg+xml" not in markdown + + +@pytest.mark.parametrize("missing_reference", [False, True]) +def test_api_reexports_and_type_links(tmp_path: Path, *, missing_reference: bool) -> None: + """Public aliases link to one complete API page, and missing targets still fail.""" + pytest.importorskip("sphinx") + pytest.importorskip("autoapi") + pytest.importorskip("sphinx_llm.txt") + pytest.importorskip("pybtex") + source = tmp_path / "docs" + source.mkdir() + package = tmp_path / "src" / "example_api" + package.mkdir(parents=True) + (package / "__init__.py").write_text('from .model import Example, model\n__all__ = ["Example", "model"]\n') + legacy_module = package / "legacy.py" + legacy_module.write_text('"""A module that will be removed between builds."""\n') + (package / "model.py").write_text( + "from __future__ import annotations\n" + "import numpy as np\n" + "from numpy.typing import NDArray\n\n" + "class Example:\n" + ' """An example with documented attributes.\n\n' + " Attributes:\n" + " weight (float): Weight used by the example.\n" + ' """\n\n' + " weight: float = 1.0\n\n" + " def sample(self, data: NDArray[np.float64], /, *, dtype: type) -> NDArray[np.float64]:\n" + ' """Return the supplied values.\n\n' + " Args:\n" + " data (NDArray[np.float64]): Values to return.\n" + " dtype (type): Type used for sampling.\n" + ' """\n' + " return data\n\n" + "def helper() -> int:\n" + ' """A supported low-level helper."""\n' + " return 1\n" + "\ndef model() -> int:\n" + ' """A function sharing its module name."""\n' + " return 1\n" + ) + inventory = tmp_path / "types.inv" + inventory.write_bytes( + b"# Sphinx inventory version 2\n# Project: External types\n# Version: 1\n" + b"# The remainder of this file is compressed using zlib.\n" + + zlib.compress( + b"numpy.typing.NDArray py:data 1 ndarray.html#numpy.typing.NDArray -\n" + b"numpy.float64 py:attribute 1 scalar.html#numpy.float64 -\n" + b"type py:class 1 type.html#type -\n" + b"float py:class 1 float.html#float -\n" + b"int py:class 1 int.html#int -\n" + ) + ) + _write_configuration( + source, + ["autoapi.extension", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx_llm.txt", "yaqs_api"], + f"autoapi_dirs = [{str(package)!r}]\n" + "autoapi_add_toctree_entry = True\n" + f'intersphinx_mapping = {{"python": ("https://example.invalid/types/", {str(inventory)!r})}}\n', + ) + (source / "index.rst").write_text( + "API documentation\n=================\n\n" + ":class:`example_api.Example` and :meth:`example_api.Example.sample` use " + ":attr:`example_api.Example.weight`.\n\n" + "See :func:`example_api.model.helper` for the low-level API.\n\n" + "A module :mod:`example_api.model` and function :func:`example_api.model` share a name.\n\n" + ".. toctree::\n\n api/index\n\n" + + ("Missing :class:`example_api.DoesNotExist`.\n" if missing_reference else "") + ) + output = tmp_path / "html" + completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - sys.executable is trusted. + [sys.executable, "-m", "sphinx", "-W", "-T", "-b", "html", str(source), str(output)], + check=False, + capture_output=True, + text=True, + timeout=60, + ) + log = completed.stdout + completed.stderr + if missing_reference: + assert completed.returncode != 0 + assert "reference target not found: example_api.DoesNotExist" in log + return + assert completed.returncode == 0, log + assert "WARNING:" not in log + child_log = re.search(r"Subprocess output available at: (.+)", completed.stdout) + assert child_log is not None, completed.stdout + assert "WARNING:" not in Path(child_log[1].strip()).read_text(encoding="utf-8") + public = (output / "api" / "example_api" / "index.html").read_text() + model = (output / "api" / "example_api" / "model" / "index.html").read_text() + index = (output / "index.html").read_text() + assert "Public imports" in public + assert 'href="model/index.html#example_api.model.Example"' in public + assert 'id="example_api.Example"' not in public + assert model.count('id="example_api.model.Example"') == 1 + assert model.count('id="example_api.model.Example.weight"') == 1 + assert "Weight used by the example." in model + assert 'id="example_api.model.helper"' in model + description = index.split("

", 1)[1].split("

", 1)[0] + for member in ("sample", "weight"): + assert f'href="api/example_api/model/index.html#example_api.model.Example.{member}"' in description + collision = index.split("A module", 1)[1].split("

", 1)[0] + assert 'href="api/example_api/model/index.html#module-example_api.model"' in collision + assert 'href="api/example_api/model/index.html#example_api.model.model"' in collision + assert 'href="https://example.invalid/types/ndarray.html#numpy.typing.NDArray"' in model + assert 'href="https://example.invalid/types/scalar.html#numpy.float64"' in model + assert 'href="https://example.invalid/types/type.html#type"' in model + markdown = (output / "api" / "example_api" / "model" / "index.html.md").read_text() + assert '/' in markdown + assert '\\*' in markdown + inventory_module = pytest.importorskip("sphinx.util.inventory") + with (output / "objects.inv").open("rb") as stream: + published = inventory_module.InventoryFile.load(stream, "", lambda _uri, location: location) + assert published["py:class"]["example_api.Example"].uri == published["py:class"]["example_api.model.Example"].uri + assert ( + published["py:method"]["example_api.Example.sample"].uri + == published["py:method"]["example_api.model.Example.sample"].uri + ) + assert ( + published["py:attribute"]["example_api.Example.weight"].uri + == published["py:attribute"]["example_api.model.Example.weight"].uri + ) + assert "example_api.model" in published["py:module"] + assert published["py:function"]["example_api.model"].uri.endswith("#example_api.model.model") + + legacy_module.unlink() + rebuilt = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - sys.executable is trusted. + [sys.executable, "-m", "sphinx", "-E", "-W", "-T", "-b", "html", str(source), str(output)], + check=False, + capture_output=True, + text=True, + timeout=60, + ) + assert rebuilt.returncode == 0, rebuilt.stdout + rebuilt.stderr + assert "WARNING:" not in rebuilt.stdout + rebuilt.stderr + with (output / "objects.inv").open("rb") as stream: + rebuilt_inventory = inventory_module.InventoryFile.load(stream, "", lambda _uri, location: location) + assert "example_api.legacy" not in rebuilt_inventory["py:module"] From 93528a0805760576c89248e4445dd87daa232d26 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:39:07 +0200 Subject: [PATCH 24/30] moved noise characterization to emulation and renamed digital twin --- docs/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 36dfba24a..022d68ac0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,8 +35,8 @@ The examples include working code and plots. | Start here | {doc}`Installation ` · {doc}`Quickstart ` | | Simulation setup | {doc}`Quantum states ` · {doc}`Hamiltonians ` · {doc}`Noise models ` · {doc}`State representations ` · {doc}`Simulation parameters ` · {doc}`Simulator configuration ` | | Simulation workflows | {doc}`Analog ` · {doc}`Digital circuits ` · {doc}`Shot-based circuits ` · {doc}`Analog-digital ` | -| Emulation | {doc}`Superconducting qubits ` · {doc}`Trapped ions ` | -| Characterization and verification | {doc}`Environmental memory ` · {doc}`Noise characterization ` · {doc}`Circuit verification ` | +| Emulation | {doc}`Digital twin ` · {doc}`Superconducting qubits ` · {doc}`Trapped ions ` | +| Characterization and verification | {doc}`Environmental memory ` · {doc}`Circuit verification ` | | Advanced examples | {doc}`Ensemble evolution ` · {doc}`Non-Markovian surrogate models (experimental) ` | | Reference and contributing | {doc}`API ` · {doc}`Citations ` · {doc}`Changelog ` · {doc}`Upgrade guide ` · {doc}`Contributing ` · {doc}`Support ` | @@ -82,6 +82,7 @@ Analog-digital simulation :maxdepth: 1 :titlesonly: +Digital twin Superconducting qubit (transmon) emulation Trapped ion emulation ``` @@ -93,7 +94,6 @@ Trapped ion emulation :titlesonly: Environmental memory characterization -Noise model characterization Circuit verification ``` From cf7c5ec505a580883e326334a431ffb95333a633 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:41:02 +0200 Subject: [PATCH 25/30] Keep local planning files out of the documentation branch --- DOCS_TODO.md | 737 ----------------------------------------------- PAPER_WRITING.md | 354 ----------------------- TODO.md | 439 ---------------------------- 3 files changed, 1530 deletions(-) delete mode 100644 DOCS_TODO.md delete mode 100644 PAPER_WRITING.md delete mode 100644 TODO.md diff --git a/DOCS_TODO.md b/DOCS_TODO.md deleted file mode 100644 index 0126150ac..000000000 --- a/DOCS_TODO.md +++ /dev/null @@ -1,737 +0,0 @@ -# YAQS 1.0 Documentation TODO - -## Goal and scope - -Make the documentation a concise guide to the workflows and options available in -YAQS. Restore reliable Read the Docs builds within the 15-minute limit. Document -the existing feature set; add no library features for this work. - -Work through the chunks in small, reviewable batches. Use the workflow-guide -order below for the detailed examples. Preserve supported workflows, scientific -interpretation, and existing page URLs. Keep detailed method explanations and -implementation details in advanced sections or the API reference. Leave -template-managed files unchanged; contribute required template fixes upstream. - -## Build baseline - -The review on 2026-10-08 covered documentation at `8684385e` and existing local -notebook caches. - -- [Read the Docs build 35013130](https://app.readthedocs.org/projects/mqt-yaqs/builds/35013130/) - at `d6e8add6` spent 116 seconds installing dependencies and reached the - 900-second timeout during Sphinx. The unfinished Sphinx command did not retain - per-notebook timings. -- The baseline documentation install included all extras, the CUDA-enabled Torch - stack, and development dependencies. -- `sphinx_llm.txt` started a second Sphinx build for Markdown. HTML and Markdown - used separate notebook caches. Both caches contain executions of identical - example code. -- An isolated HTML render at `8684385e`, with notebook execution and Markdown - generation disabled, took 39 seconds locally. Strict checks failed on - documentation warnings; this was not a successful full documentation build. - -Historical local cache timings, mostly from Python 3.12: - -| Notebook | HTML execution | Markdown execution | -| ---------------------- | -------------: | -----------------: | -| Quickstart | 176 s | 173 s | -| Noise characterization | 27 s | 29 s | -| Memory surrogate | 15 s | 17 s | -| Environmental memory | 13 s | 14 s | -| All 19 notebooks | 322 s | 323 s | - -These are execution totals from separate historical runs, not elapsed time for -one build or current RTD timings. Code hashes match 17 of the 19 current -notebooks, but library versions and execution environments have changed. Fresh -profiling must establish the remaining bottlenecks. - -## Chunk 1: Restore a fast documentation build - -- [x] Make Markdown generation reuse the HTML build's parsed notebooks and - outputs. Start with `llms_txt_build_parallel=False` and an explicit shared - `nb_execution_cache_path` in `docs/conf.py`. Preserve the MQT LLM files, - including an explicit `llms_txt_full_build=True`. -- [x] Align local Nox and RTD documentation installation. Omit development and - test dependencies, use CPU-only Torch, and retain dependencies needed by - the OpenQASM 3 and surrogate examples. -- [x] Bound numerical threads and process workers in the documentation build - environment. Preserve ordinary examples' default parallel execution and - suppress progress bars in rendered documentation. -- [x] Measure a cold build. Record the commit, environment, dependency-install - time, per-notebook execution time, HTML and Markdown generation time, and - total build time. Confirm that each notebook executes only once. -- [x] Reduce example workloads only where fresh timings justify it. Preserve - meaningful outputs and coverage of supported optional paths. Keep - expensive numerical validation in the appropriate test or - release-validation tier. The local cold build met the target without - reducing workloads. -- [x] Remove `htmlzip` if the downloadable archive is not needed. - -Acceptance: a cold RTD build completes in roughly 10 minutes or less, leaving -headroom below the 15-minute limit. Required examples and optional paths remain -validated. Cached builds alone do not satisfy this check. - -### Chunk 1 validation: 2026-10-08 - -The final local cold build used `8684385e` plus the configuration changes above, -Python 3.14.2, a fresh environment, and empty uv, notebook, Numba, and Sphinx -caches. The build completed in **9 minutes 50 seconds**. - -| Phase | Time | -| ---------------------------------- | ----: | -| Dependency installation | 200 s | -| HTML parsing, execution, rendering | 372 s | -| Markdown generation | 3 s | -| LLM file combination and shutdown | 15 s | -| Total, including environment setup | 590 s | - -Notebook execution took 335 seconds within the HTML phase. All 19 notebooks -executed once; Markdown executed none. The slowest guides were circuit -observables (75 s), noise characterization (44 s), and analog simulation (41 s). -No workload reductions were needed. The build retained 33 SVG plot outputs, -per-page Markdown, `llms.txt`, and `llms-full.txt`. No notebook produced an -execution error or progress bar. - -The environment contained CPU-only Torch and the OpenQASM 3 importer, with no -CUDA, Triton, development, or test packages. The strict integration test passed -with these resolved documentation dependencies. Full lint and the new-file hooks -passed. - -The full build did not use `-W`. The parent build reported 1,443 warnings and -the Markdown child reported 1,204 warnings; chunk 4 must resolve these. Cold RTD -validation remains pending after publication of the changes. - -Local evidence, including source hashes, package versions, full notebook -timings, logs, and generated pages: -[/tmp/yaqs-docs-chunk1-final-8684385e/report.md](/tmp/yaqs-docs-chunk1-final-8684385e/report.md). - -## Chunk 2: Shorten first-use and configuration guides - -- [x] Keep `quickstart.md` as a compact tour of the main use cases. Showcase - useful scales within the documentation budget, with scientifically - meaningful plots and a consistent journal style. Keep analog, digital, - analog-digital, equivalence, memory, noise fitting, and surrogate - examples. Fold plotting code, link to dedicated guides, and omit - unnecessary advanced settings. -- [ ] Simplify `simulation_parameters.md`: one preset table, one override - example, and short analog and digital recipes. Explain when to change a - setting and what the change affects. Remove repeated override rules and - move gate-update mechanics into an advanced section. -- [ ] Simplify `simulator_initialization.md`: common controls and one reusable - example first. Move CPU-discovery details, process internals, and retry - customization into an advanced section. Move the result catalogue into the - results guide in chunk 3. -- [ ] Start `custom_gates.md` with the common task of supplying a custom - unitary. Put DAG translation, `BaseGate` fields, manual gate construction, - and generator details later in the page. -- [ ] Start `hamiltonians.md` with the built-in model catalogue and construction - examples. Move energy and correlation contractions after model selection. -- [ ] Consolidate backend-selection explanations. Use one main representation - guide and link to it from state, Hamiltonian, and workflow pages. -- [ ] Extend the quickstart examples into the dedicated workflow guides using - the checklist below. Replace competing introductory examples with one main - worked example per guide. Preserve distinct supported workflows in focused - later sections or linked advanced guides. -- [ ] Preserve units, time grids, spatial ordering, shot and trajectory budgets, - accuracy tradeoffs, supported restrictions, and the meaning of scientific - diagnostics. Move deeper explanations rather than remove needed context. - -Acceptance: each guide leads with its purpose, a small working example, the main -user choices, and how to read the output. Detailed signatures remain in the API -reference. All existing capabilities remain discoverable. - -### Workflow guides: extend the quickstart examples - -Keep the quickstart as a compact tour. Each detailed guide should stand alone, -use the corresponding quickstart model and terminology, and explain each step in -the order a user performs it. Work through one guide at a time in the order -below. Extend the scientific question and user choices before increasing system -size, trajectories, sweeps, or training cost. - -Follow [PAPER_WRITING.md](PAPER_WRITING.md) for prose and structure. Begin with -the physical question, narrow to the setup and technical steps, then return to -what the results mean and where the conclusion stops. Connect paragraphs through -the questions the reader needs answered. Keep the instructional steps, but avoid -clipped prose, filler, and repeated claims. - -Shared structure for each guide: - -1. State the task, expected result, and prerequisites, including optional - extras. -2. Build the model, initial state, and noise or controls. Explain their physical - meaning, units, site ordering, and the supported input forms used here. -3. Choose accuracy and sampling settings. Explain the few settings that matter - for this task and link to the setup guides for the full options. -4. Initialize the public interface, run the calculation, and extract the output. - Explain the relevant result fields, array axes, and ordering beside the code. -5. Read the figure, add one useful extension or reference check, and explain the - limits of the conclusion. End with a short options summary and related - guides. - -Keep working code visible and plotting code folded. Use the quickstart's journal -figure style, clear panel labels, units, and shared scales for comparisons. -Explain decisions and interpretation; keep algorithm derivations, full -signatures, and internal helpers in advanced sections or the API reference. -Preserve default parallel execution, show separate initialization and run calls, -and suppress progress only for the documentation. Explain the script entry-point -guard where relevant. Use supported public imports. - -Where supported, extend each walkthrough with a small noise-strength comparison: -include a noiseless baseline and several clearly different strengths, such as -weak, intermediate, and strong noise. Keep the Hamiltonian or circuit, initial -state, time grid, and accuracy settings fixed. Use comparable sampling budgets, -label rates or probabilities correctly, and explain sampling uncertainty. Use -shared axes and color scales so each figure shows the physical change rather -than a change in normalization. Choose strengths after checking that the effect -is visible and the runtime is reasonable. Distinguish added Markovian noise from -the effects of coupling to an explicit environment. - -#### 1. Analog simulation — `docs/examples/analog_simulation.md` - -- [x] Expand the 20-site excitation-transport example: construct the XY - Hamiltonian, prepare the localized excitation, define relaxation, select - observables and time grid, and run noiseless and noisy simulations. -- [x] Explain occupation heatmaps and total excitation, including boundary - reflections, relaxation, and trajectory fluctuations. Keep the analytic - decay comparison and distinguish an ensemble expectation from a finite - trajectory estimate. -- [x] Finish with a row or grid of occupation heatmaps for zero, weak, - intermediate, and strong relaxation, with shared time and site axes and - one color scale. Reuse the baseline runs. Compare total excitation on a - companion plot and explain which transport features relaxation suppresses. - Explain how to choose trajectory count, accuracy preset, and system size. - Preserve distinct existing capabilities through later sections or links. - -The comparison uses relaxation rates 0, 0.5, 1.5, and 4 with one shared color -scale. The nonzero rates span lifetimes from 2 to 1/4, giving visible -differences in how much excitation survives during transport. The guide follows -the physical question through the setup to the interpretation and limits of the -result. - -Validation on 2026-10-09: all nine guide cells executed in 67 seconds in the -existing Python 3.12 documentation environment with capped threads and two -workers. The noiseless/strong-relaxation pair took 22 seconds; the additional -weak/intermediate runs took 43 seconds. Independent single-excitation dynamics -gave a maximum coherent occupation error of 0.0051. Checks passed for result -axes, trajectory averages, excitation conservation, conditional spatial -profiles, and analytic decay within finite-sample uncertainty at all four rates. - -The isolated strict HTML build passed, and both figures were inspected. Plotting -cells were folded, Markdown reused execution, and LLM files remained available. -Two SVG MIME-priority warnings remain in the Markdown child for chunk 4. Linked -guide paths were checked but their contents were stubbed in this isolated build; -full-site and final cold-build validation remain pending. - -Preview, raw trajectory data, CSV references, figures, timings, and logs: -[/tmp/yaqs-analog-guide/report.md](/tmp/yaqs-analog-guide/report.md). - -#### 2. Circuit measurements — `docs/examples/circuit_shots.md` - -- [x] Expand the 16-qubit circuit example: build the circuit, prepare the input, - define damping, set shots and accuracy, initialize the simulator, and - collect noiseless and noisy counts. -- [x] Explain outcome encoding and how counts become probabilities and grouped - excitation-number histograms. Distinguish shots from trajectories and - explain finite-sample fluctuations without promising identical histograms. -- [x] Compare grouped readout histograms at zero and several damping strengths, - keeping the circuit and shot budget fixed. Explain the shift in excitation - number and finite-sample fluctuations. Keep individual outcomes - discoverable alongside the grouped histogram. Base - `circuit_observables.md` on the analog XY transport example, reconstruct - its dynamics with exchange gates and mid-circuit observable checkpoints, - and compare noiseless and noisy results. Explain how circuit noise - strengths relate to the represented time step. Retain OpenQASM inputs and - gate-application choices in focused later sections. - -The shot guide uses a 16-qubit graph state and rates 0, 0.1, 0.5, and 1.5, -comparing complete excitation-number histograms with a shared sampled baseline. -The observable guide uses the analog example's 20-site XY chain, localized -excitation, time grid, and rates 0, 0.5, 1.5, and 4. Symmetric Trotter steps -reconstruct the transport heatmaps. Per-site gate counts set noise strengths so -each step accumulates the intended relaxation exposure. A companion figure -compares analog and digital profiles, Trotter-step refinement, and excitation -survival. Sixteen noisy trajectories keep the observable example within the -documentation budget. Plotting code is folded. - -Validation on 2026-10-09 used the existing Python 3.12 documentation environment -with capped threads and two workers. The six shot-guide cells took 77 seconds; -checks covered Qiskit probabilities, the exact noiseless binomial distribution, -weak-noise marginals within sampling uncertainty, bit encoding, and count -totals. The nine observable-guide cells took 104 seconds. Against independent -exact single-excitation dynamics, the maximum coherent occupation error fell -from 0.0070 to 0.0017 when the circuit step halved. The analog reference error -was 0.0051. Noisy outputs matched an independent finite-circuit channel -reference within sampling uncertainty, with conditional spatial-profile errors -below 0.000001. Trajectory aggregation, checkpoint axes, excitation conservation -or loss, OpenQASM counts, and both gate modes also passed checks. - -The isolated strict HTML builds passed and all three figures were inspected. -Markdown reused execution, and LLM files remained available. SVG MIME-priority -warnings remain in the Markdown children for chunk 4. Linked guide paths were -checked but their contents were stubbed; full-site and final cold-build -validation remain pending. - -Previews, raw data, CSV files, figures, timings, and logs: - -- Shots: - [/tmp/yaqs-circuit-guides/report.md](/tmp/yaqs-circuit-guides/report.md). -- Observables: - [/tmp/yaqs-digital-xy-guide/report.md](/tmp/yaqs-digital-xy-guide/report.md). - -#### 3. Circuit verification — `docs/examples/equivalence_checking.md` - -- [x] Expand the quickstart comparison into hardware-constrained compilation, a - deliberate rotation-angle bug, and a noise-strength sweep. Rename the - guide and sidebar entry to Circuit Verification. -- [x] Explain checker setup, output-layout alignment, returned overlap, decision - thresholds, and sampling uncertainty. Compare correct and faulty - compilations across noiseless, weak, and stronger Pauli noise on shared - axes. Distinguish the offline target and assumed noise from measured - device data. -- [x] Keep backend selection, OpenQASM inputs, and execution controls as short - option sections after the worked example. Validate against independent - unitary and noisy-channel references and remove unsupported performance - claims. - -The guide compiles a four-qubit circuit for a nearest-neighbor target using -native `rz`, `sx`, `x`, and `cx` gates. Routing increases the controlled-X count -from three to nine. The reference includes the final output permutation; without -this alignment the overlap is 0.25, while the aligned comparison gives one. The -compiled circuit keeps its physical gate sequence for the noise sweep. A native -rotation-angle error follows the exact cosine overlap. Correct and faulty -compilations then use six Pauli-error probabilities and 256 trajectories per -point. Plotting code is folded, and default backend selection and parallel -execution remain enabled. - -Validation on 2026-10-09 used the existing Python 3.12 documentation environment -with capped numerical threads and two workers. All five code cells executed in -about 10 seconds, including independent checks. Qiskit's layout-aware unitary -verified the compilation; dense operators checked every angle point. Exact -Qiskit channels summed the Pauli branches at every eligible gate for both -implementations. Sampled fidelities differed from these references by less than -1.6 standard errors. Additional checks covered native connectivity, retained -trajectory aggregation and error estimates, reproducibility across worker -counts, and agreement between matrix and MPO backends. - -The isolated strict HTML build passed and the figure was inspected. Markdown -reused execution and LLM files remained available. SVG MIME-priority and -bibliography-node warnings remain in the Markdown child for chunk 4. Linked -guide paths were checked but their contents were stubbed; full-site and final -cold-build validation remain pending. - -Preview, raw data, exact references, CSV files, figure exports, timings, and -logs: -[/tmp/yaqs-verification-guide/report.md](/tmp/yaqs-verification-guide/report.md). - -#### 4. Environmental memory — `docs/examples/characterization.md` - -- [x] Expand the coupling sweep: define the system and environment, configure - the probing schedule and cut, run characterization, and extract spectra - and entropy from the results. -- [x] Explain normalized spectral weights, entropy, and what they reveal about - the response to the chosen probes. Distinguish these diagnostics from - environment populations and mixed-state Schmidt spectra. Explain the - nonmonotonic coupling result without claiming a universal memory measure. -- [x] Explain the main sampling and intervention choices. Keep conditioned reset - delay, response-mode inspection, and process-tensor diagnostics as focused - sections, preserving the `memory-theory` and `reset-delay` anchors. -- [x] Add a small dephasing comparison through dense process-tensor tomography, - then characterize the reconstructed tensors. Explain that the direct - Hamiltonian path does not accept a `NoiseModel`, and distinguish coupling - changes from added Markovian noise. Validate the reconstruction and limit - claims about small noisy spectral weights. - -The main example uses three Ising spins, with site 0 as the probe and two spins -as its environment. Thirteen couplings share one 8-by-8 probe grid, four -interventions, and cut 2. The guide explains the five evolution intervals, -outcome-probability weighting, response-matrix axes, retained singular values, -entropy, and effective modes. A red spectrum sweep and entropy plot reproduce -the quickstart's nonmonotonic result. Seven explicit reset delays show -conditioned persistence without implying an all-outcome memory length. - -The added-noise example uses two spins and one intervention to keep dense -tomography small. Each noisy reconstruction uses 16 sequences and 512 -trajectories per sequence, with integration step 0.025. Dephasing rates 0, 1, -and 4 concentrate the response in the leading mode. A separate temporal-entropy -calculation explains its distinction from probe-response entropy. Public -imports, automatic representation selection, and default parallel execution -remain in the examples. Plotting code is folded. - -Validation on 2026-10-09 used the existing Python 3.12 documentation environment -with capped numerical threads and two workers. All nine cells executed in 45 -seconds, including independent checks. Explicit spin-Hamiltonian exponentials -and selected-branch density-matrix evolution reproduced every response-matrix -entry in the coupling and delay sweeps within 0.00000005. Checks also covered -probe reuse, SVD reconstruction, tail weight, entropy, effective modes, and the -uncoupled rank-one limit. - -An independent continuous-time Lindblad generator reproduced the qualitative -noise effect. The noisy response matrices differed from this reference by at -most 0.022 and 0.031, including finite-step and sampling error. The strongest -noise's small entropy remains sensitive to the sampling floor; the guide does -not interpret every retained tail mode as physical memory. Reconstructed tensors -passed explicit Hermiticity, positivity, and causal-normalization checks. -Noiseless dense and uncapped direct-MPO tensors agreed, including response -matrices and temporal entropy. - -The isolated strict HTML build passed and all three figures were inspected. -Markdown reused execution and LLM files remained available. Three SVG -MIME-priority warnings remain in the Markdown child for chunk 4. Linked guide -paths were checked but their contents were stubbed; full-site and final -cold-build validation remain pending. - -Preview, raw matrices, exact references, CSV files, figure exports, timings, and -logs: [/tmp/yaqs-memory-guide/report.md](/tmp/yaqs-memory-guide/report.md). - -#### 5. Noise characterization — `docs/examples/digital_twin.md` - -- [x] Expand the four-site transport example: generate synthetic dynamics, - select endpoint observations, define candidate relaxation and dephasing - channels, choose initial guesses and bounds, and fit the rates. -- [x] Explain observation axes, time alignment, observable selection, and - parameter order and meaning. State that channel types and locations are - assumed known; fitting strengths does not discover an arbitrary model. -- [x] Rerun the fitted model, compare dynamics on shared heatmap scales, and - validate withheld interior observables. Explain how measured data and - sampling uncertainty replace synthetic input. Keep stochastic fitting and - optimizer controls as short options after the worked example. -- [x] Show zero, weaker, reference, and stronger noise with matched occupation - heatmaps. Fit only the reference case and reuse its fitted dynamics for - validation. Explain the distinct physical effects of relaxation and - dephasing without attributing their combined sweep to one channel alone. - -The guide fits two local Lindblad rates in a four-spin XY chain from endpoint Z -traces. It explains the initial excitation, observation grid, jump operators, -rate bounds, mean-squared objective, and fitted result. Two figures show the -noise-strength comparison, reference and fitted transport, and predictions at -the withheld interior sites. All heatmaps share a square-root color scale. Only -one optimization runs. Public imports, automatic fitting-backend selection, and -default parallel settings remain. Plotting code is folded. - -Measured-data guidance covers observable and time ordering, converting -occupations to Z expectations, equal objective weights, finite-shot uncertainty, -model assumptions, and limits on identifiability and extrapolation. -Forward-model and optimizer options explain stochastic sampling, random seeds, -scalar search, and result fields without further fits or promises of future -features. - -Validation on 2026-10-09 used the existing Python 3.12 documentation environment -with capped numerical threads and two workers. All eight cells executed in 14 -seconds, including independent checks; the fit took nine seconds. Fitted rates -were 0.3499 and 0.1199 for synthetic rates 0.35 and 0.12. Endpoint Z-trace RMSE -fell from 0.069 to 0.00011, and withheld interior RMSE was 0.000071. - -An independent five-state vacuum-plus-single-excitation master equation, -integrated with SciPy DOP853, reproduced every site and time sample at all four -noise scales and for the fitted rerun within 0.000000000012. Checks also covered -trace, positivity, excitation conservation or loss, initial-state placement, -observation ordering, fit versus rerun consistency, process ordering, unchanged -initial guesses, and optimizer losses. Separate channel references confirmed -that dephasing preserves population while changing transport. A local endpoint -sensitivity check found distinct rate signatures; this does not establish global -identifiability or experimental confidence intervals. - -The isolated strict HTML build passed and both figures were inspected. Markdown -reused execution and LLM files remained available. Two SVG MIME-priority -warnings remain in the Markdown child for chunk 4. Linked guide paths were -checked but their contents were stubbed; full-site and final cold-build -validation remain pending. - -Preview, raw dynamics, independent references, CSV files, figure exports, loss -history, timings, and logs: -[/tmp/yaqs-noise-guide/report.md](/tmp/yaqs-noise-guide/report.md). - -#### 6. Experimental surrogate models — `docs/examples/memory_surrogate.md` - -- [x] Expand the random-control training and unseen pulse-angle sweep through - public `MemoryCharacterizer.sample`, `train`, and `predict` calls. Explain - the environment preparation, intervention schedule, training set, - checkpoint-selection validation set, and chosen prediction sequences. -- [x] Explain the final-state Bloch-plane plot and coherence comparison with - free evolution. Add an independent small-system reference comparison; - report prediction errors and check trace, Hermiticity, and positivity - without concealing errors through clipping or projection. -- [x] Keep the experimental and publication-status note. Explain that random - validation accuracy does not certify chosen controls or longer horizons. - Keep this example within the validated two-intervention horizon; reliable - long-protocol generalization is separate work, not a documentation - promise. -- [x] If training and validation cost permit, compare the control response at - weak and stronger system-environment coupling. Train and validate a - separate model for each Hamiltonian; the current model does not take - coupling strength as a prediction input. The public training path does not - accept a `NoiseModel`, so describe this as an environmental-coupling - comparison. Keep added-noise training outside the documentation scope. - -The guide now trains separate models at $J=0.3$ and $J=1$ using 4,096 random -unitary sequences and 256 checkpoint-selection sequences per Hamiltonian. The -schedule contains two interventions and two evolution intervals of 0.6. Both -models predict the same 61-angle pulse sweep from a probe in $|+\rangle$ and an -environment in $|0\rangle$. The worked example uses only public YAQS imports, -retains default parallel data generation, and suppresses documentation progress -bars. It replaces the repeated zero-evolution training examples with one -training loop, a control-response plot, and a coupling/reference comparison. - -All 7 production code cells matched the executed notebook. In the existing -Python 3.12 documentation environment, capped to one numerical thread and two -workers, execution took 171.5 seconds including independent checks; the -two-model sampling and training cell took 167.2 seconds. The direct reference -builds the Hamiltonian from Pauli matrices and evolves the joint state with -SciPy's matrix exponential. An explicit index sum independently checks the -partial trace. Checks also cover site ordering, schedule, both intervention -outputs, periodic pulse endpoints, array shapes, finite values, and unchanged -probe preparation. - -For $J=0.3$ and $J=1$, coherence RMSE was 0.0179 and 0.0339, respectively. The -maximum half-trace-norm matrix errors were 0.0513 and 0.0593. All returned -matrices had positive eigenvalues in these sweeps, with maximum trace errors -0.0062 and 0.0092. The facade makes estimates Hermitian; it does not enforce -normalization or positivity. The guide checks and reports these properties -without clipping, renormalization, or projection. It retains the experimental -and unpublished status, the fixed environment and schedule, and the limits of -generalization beyond the tested unitary controls and horizon. - -The isolated strict HTML build passed and both SVG figures were inspected. -Markdown reused notebook execution and LLM files remained available. The 2 SVG -MIME-priority warnings in the Markdown child remain tracked for chunk 4. The -two-model comparison adds training cost; its place in the total 15-minute budget -must be checked in the final full-site cold build. These isolated runs use -existing dependencies and compilation caches, and linked page contents were -stubbed. They do not establish cold RTD build time. - -Preview, raw predictions, independent reference states, CSV data, trained state -dictionaries, figure exports, timings, and logs: -[/tmp/yaqs-surrogate-guide/report.md](/tmp/yaqs-surrogate-guide/report.md). - -#### 7. Analog-digital simulation — `docs/examples/digital_analog_simulation.md` - -- [x] Replace the one-qubit example with a 20-site XY excitation echo. Prepare - the excitation with a circuit, alternate analog intervals and staggered - phase pulses, and explain how the final pulse restores the phase frame. -- [x] Explain program-wide settings, local segment outputs, instantaneous gate - timestamps, and occupation extraction across repeated analog boundaries. - Retain supported program options in a short final section. -- [x] Compare the noiseless echo with uniform relaxation at rate 0.5 and local - Pauli-Z dephasing at rates 0.05 and 0.2. Use shared heatmap scales and - pointwise standard errors. Explain the difference between excitation loss - and loss of refocusing without equating the channel strengths. -- [x] Execute all cells, inspect the figures, and compare the plotted dynamics - with an independent Lindblad reference. Keep the final full-site and cold - RTD build checks pending. - -All 7 production cells matched the executed notebook. Execution took 79.3 -seconds in the existing Python 3.12 documentation environment, with one -numerical thread and two workers. The three noisy simulations took 75.2 seconds -together. The independent reference constructs the nearest-neighbor hopping -matrix and Lindblad generator in the vacuum and single-excitation sector, then -applies the phase pulses explicitly. It uses no YAQS propagation or Hamiltonian -helpers. - -The noiseless return was 0.99994, against the exact value 1. The maximum -occupation error over all coherent site/time samples was 0.0051. With -relaxation, the final population was 0.21875 against the exact value 0.22313. -The two dephasing returns were 0.650 and 0.341, against reference values 0.719 -and 0.350; their estimated standard errors were 0.068 and 0.059. All noisy -profiles passed the recorded finite-ensemble bounds. Checks also covered -input-state preservation, observable ordering, trajectory means, segment -continuity, timelines, and population conservation under dephasing. Population -uncertainty was calculated after summing sites within each trajectory. - -The isolated strict HTML build passed and both SVG figures were inspected. -Markdown reused execution and LLM files remained available. Three MIME-priority -warnings remain in the Markdown child: two figure outputs and one text output. -These remain tracked for chunk 4. Linked page paths were checked, but their -contents were stubbed. The run used existing dependencies and warm compilation -caches, so it does not establish full-site or cold RTD build time. - -Preview, raw trajectories, independent references, CSV data, figure exports, -source hash, versions, timings, and logs: -[/tmp/yaqs-hybrid-guide/report.md](/tmp/yaqs-hybrid-guide/report.md). - -#### Preserve other workflows and validate each guide - -- [ ] Keep analog-digital programs, custom gates, hardware models, scheduled - jumps, and ensembles discoverable. Reuse setup and terminology where - useful; retain their distinct worked examples rather than force them into - a quickstart example that does not cover their purpose. -- [ ] Execute each revised guide independently and inspect its figures. Check - public imports, result interpretation, meaningful numerical references, - links, and lint before Aaron reviews the guide. -- [ ] Record per-guide execution time and cumulative cold-build cost. Quickstart - and detailed notebooks execute separately; matching code snippets alone do - not share execution. Reuse expensive fits and trained models within each - guide, and avoid extra runs merely to produce another plot. -- [ ] Remove superseded introductory examples and repeated option catalogues - only after checking that all distinct supported capabilities remain - documented. Update quickstart links and the homepage task table as needed. - -Acceptance: each main quickstart example has a clear, independently runnable -walkthrough with an explained extension or validation. Include a meaningful -noise-strength comparison wherever the public workflow and build budget allow -it; document the supported alternative where they do not. The guide teaches -users how to adapt the workflow, retains its scientific limits, and fits the -final cold-build budget. Complete the full-site validation in chunk 4 after the -guide reviews. - -### Quickstart validation: 2026-10-08 - -The six workflows executed in 136 seconds in the existing Python 3.12 -documentation environment. Noiseless and noisy transport on 20 sites took 23 -seconds; 16-qubit readout took 26 seconds; endpoint noise fitting and its -simulation rerun took 9 seconds; surrogate training and its pulse-angle sweep -took 76 seconds. Equivalence and memory sweeps together took 3 seconds. The -isolated strict HTML build took 142 seconds. - -Numerical checks passed for excitation conservation, circuit sampling and its -damping shift, analytic equivalence overlaps, memory spectra, and fitted noise -rates. Earlier independent transport and withheld-site noise checks are in the -comparison report linked below; those five example workflows are unchanged. - -The surrogate uses only public `MemoryCharacterizer.sample`, `train`, and -`predict` calls. It trains on 4,096 random unitary sequences and selects a -checkpoint using 256 random validation sequences. One model predicts 61 chosen -Z-pulse angles, applied between two evolution intervals. The figure shows final -coherence against pulse angle and the predicted free-evolution baseline. - -Private matrix-exponential references give coherence RMSE 0.034 and maximum -error 0.078 across the sweep. Complex density-matrix entry RMSE is 0.032. The -predictions are unmodified, Hermitian, and positive in this example, with trace -errors below 0.005. The zero and full-turn pulses give the same prediction. The -smaller training budget failed fresh sweep checks. This validates the shown -short-horizon coherence sweep, not arbitrary controls, long horizons, or exact -density-matrix reconstruction. Independently check the surrogate guide's -chosen-control examples during its review. - -All 15 production code cells matched the executed notebook. Six SVG figures -rendered, plotting and training cells were folded, and LLM files remained -available. The surrogate figure shows final probe states in the Bloch plane, -colored by pulse angle, beside a coherence sweep with shaded gains and losses. A -section note marks surrogate modeling as experimental and not yet supported by a -published YAQS paper. The updated figure was inspected. Other figures were -inspected in the earlier comparison build. The Markdown child retains six SVG -MIME-priority warnings for chunk 4. This used warm numerical caches; final -full-site and cold RTD validation remain pending. - -Current plot, pulse-sweep data, package versions, timings, and logs: -[/tmp/yaqs-quickstart-pulse-sweep/report.md](/tmp/yaqs-quickstart-pulse-sweep/report.md). - -Earlier independent checks and longer-horizon training trials: -[/tmp/yaqs-quickstart-generalization/report.md](/tmp/yaqs-quickstart-generalization/report.md). - -### Analog-digital quickstart addition: 2026-10-09 - -The quickstart now includes the 20-site XY excitation echo with three occupation -heatmaps: free evolution, refocusing, and refocusing with Pauli-Z dephasing at -rate 0.2. All panels share one scale. The setup uses public imports, separate -simulator initialization, default parallel execution, and 32 noisy trajectories. -Plotting code is folded, and the section links to the detailed program guide. - -All 17 production cells matched the executed notebook. The complete quickstart -executed in 160.9 seconds; the new simulation cell took 15.8 seconds. Numerical -checks passed for all seven workflows. The noiseless return was 0.99994, and the -dephased return was 0.341 against an independent Lindblad reference of 0.350, -with estimated standard error 0.059. Checks covered every new heatmap sample, -the plotted arrays, input preservation, segment continuity, observable order, -trajectory means, and excitation conservation on each trajectory. - -The isolated strict HTML build passed. The new figure was inspected, all seven -SVG figures rendered, and Markdown reused execution. LLM files remain available. -The Markdown child retains seven SVG MIME-priority warnings for chunk 4. This -run used the existing Python 3.12 documentation environment with one numerical -thread, two workers, and warm compilation caches. Linked page contents were -stubbed; full-site and cold RTD validation remain pending. - -Preview, numerical checks, raw echo trajectories, independent references, CSV -data, figure exports, source hash, versions, timings, and logs: -[/tmp/yaqs-quickstart-echo/report.md](/tmp/yaqs-quickstart-echo/report.md). - -## Chunk 3: Fill practical gaps and correct claims - -- [ ] Add one supported-combinations overview. Consolidate representation, - noise, circuit, diagnostic, ensemble, piecewise-program, and - characterization restrictions. Link to existing guides for details. -- [ ] Add a short results guide covering observable ordering, array axes, time - grids, counts, requested versus executed trajectory counts, diagnostics, - spectra, final states, and program segments. Explain that averaged noisy - Schmidt spectra describe pure trajectories, not a mixed-state spectrum. -- [ ] Explain when outputs are populated and which combinations are supported. - Keep result-field details accurate without adding a persistence API. -- [ ] Document pickle as trusted, temporary, same-version checkpoint storage. - Avoid promises of portable or versioned persistence. -- [ ] Add a complete parallel script example with an - `if __name__ == "__main__":` guard. Explain notebook execution separately - and keep automatic process-context guidance current. -- [ ] Correct reproducibility claims, including the limits of `State(seed=...)`. - Use supported seeds where examples need repeatable stochastic results. -- [ ] Correct claims about configuration mutation and automatic backend - selection against the implementation. -- [ ] Supply existing evidence for the equivalence-performance crossover claim, - including the referenced benchmark script, or remove the claim. Describe - the configured automatic cutoff as a heuristic. -- [ ] Use supported public imports in ordinary examples. Mark intentionally - documented low-level interfaces clearly without exporting extra helpers - solely for documentation. - -Acceptance: users can choose a supported workflow, interpret its output, and -understand relevant limitations. No major feature needs another broad tutorial. - -## Chunk 4: Simplify navigation and complete validation - -- [ ] Align the documentation homepage's title and introduction with the README. - Remove the unsupported "under a minute" quickstart promise. Shorten the - 21-row learning-path table to the main user tasks. Describe executable and - static examples accurately. -- [x] Regroup sidebar navigation while preserving existing page URLs: - -| Group | Contents | -| --------------------------------- | --------------------------------------------------------------------------- | -| Start here | Installation and quickstart | -| Simulation setup | States, Hamiltonians, noise, representations, presets, execution, results | -| Simulation workflows | Analog, circuits, shots, analog-digital programs | -| Characterization and verification | Memory, noise fitting, circuit equivalence | -| Advanced examples | Ensembles, scheduled jumps, custom gates, hardware, experimental surrogates | -| Reference and contributing | API, citations, changelog, upgrading, development, support | - -- [ ] Curate API navigation around the stable public interface. Remove duplicate - object indexing and unresolved targets. Keep implementation helpers from - overwhelming the public reference and preserve needed canonical links. -- [ ] Fix the unsupported Mermaid directive, notebook metadata and lexer - warnings, document and method references, and remaining citation or - included-file references. The bibliography directive was repaired in the - README/reference update; verify the merged version rather than repeat it. -- [ ] Add a fast strict documentation check to CI. Pass a fresh - `sphinx-build -E -a -n -T -W --keep-going` check without blanket warning - suppression. Scope necessary external-reference exceptions narrowly. -- [ ] Execute all documentation notebooks in a clean environment, including - optional examples. Validate supported imports and meaningful outputs - without duplicating the numerical test suite. Record execution timings. -- [ ] Run `uvx nox -s docs -- -b linkcheck`. Fix broken project links and record - necessary exceptions for unavailable external sites. -- [ ] Inspect rendered desktop and narrow-screen pages: navigation, code, - figures, tables, diagrams, API links, citations, and release notes. -- [ ] Run `uvx nox -s lint` after each batch of changes. -- [ ] Confirm that RTD builds the final reviewed documentation successfully. -- [ ] Complete Aaron's documentation review and resolve substantive findings. - -Acceptance: users can find each supported capability through the sidebar and -task links. Strict checks, example execution, link checking, rendered-page -inspection, and a cold RTD build pass for the reviewed commit. - -### Navigation validation: 2026-10-09 - -The sidebar uses the six groups above with short labels. All 29 existing page -targets remain present once, and page URLs are unchanged. The results guide can -join Simulation setup when chunk 3 adds it. - -A full HTML render with notebook execution disabled succeeded. Rendered sidebar -checks passed on the homepage, quickstart, equivalence guide, and API root: each -page retains all six groups in order and links to all existing targets. This -check omitted external inventories and did not use `-W`; the build retained 477 -documentation warnings. Full strict validation and responsive browser inspection -remain pending. Full lint and the planning-file hooks passed. - -Navigation preview and evidence: -[/tmp/yaqs-docs-navigation/html/index.html](/tmp/yaqs-docs-navigation/html/index.html), -[/tmp/yaqs-docs-navigation/record.json](/tmp/yaqs-docs-navigation/record.json), -and -[/tmp/yaqs-docs-navigation/build.log](/tmp/yaqs-docs-navigation/build.log). diff --git a/PAPER_WRITING.md b/PAPER_WRITING.md deleted file mode 100644 index 45d734153..000000000 --- a/PAPER_WRITING.md +++ /dev/null @@ -1,354 +0,0 @@ -# Scientific Paper Writing Guide - -Use this guide to draft or revise technical research papers for clarity, coherence, and scientific precision. The goal is not to make every paper sound the same. It is to make the reasoning easy to follow while preserving the authors' voice and the subject's necessary technical detail. - -Treat revision as a careful human line edit, not as a general rewrite. Simplify the explanation itself rather than mechanically replacing technical words with supposedly simpler synonyms. - -## 1. Start with the scientific story - -Before editing sentences, write the paper's story in five to seven plain statements: - -1. What broad problem matters? -2. What is already known? -3. What remains unresolved? -4. What does this paper do? -5. What evidence answers the unresolved question? -6. What is the main result? -7. What remains limited or unknown? - -Every major section should advance this story. Remove, shorten, or relocate material that does not help the reader understand or evaluate it. - -The storyline is not a list of everything done during the project. It is the shortest defensible chain from the motivating problem to the conclusion supported by the evidence. - -### Use a V-shaped structure - -The paper should narrow and then widen: - -1. Begin with the general scientific problem and why it matters. -2. Narrow to the specific gap, construction, and tests addressed by the paper. -3. End by returning to the broader meaning, limitations, and practical consequences. - -Use the same shape within major sections whenever possible. Open with the high-level question and its role in the paper, move into the necessary technical detail, and close with the answer and a transition to the next question. This is a logical structure, not a demand for artificial symmetry. - -### Respect the reader's knowledge order - -Present ideas in the order needed to understand them. Do not refer to a proof, proposition, mechanism, result, or technical distinction before it has been introduced. The abstract and introduction may preview the main outcome in ordinary language, but they should not depend on later notation or unexplained labels. - -A transition should normally identify the question that remains after the current section. It should motivate the next section without giving its detailed answer in advance. At every transition, ask what the reader knows at that point and what they need to learn next. - -## 2. Separate prior work from the present contribution - -Make the boundary unmistakable. - -- Describe established knowledge in neutral, factual prose. -- Explain the specific gap before presenting the new work. -- Begin the contribution with a clear transition such as “Here, we…” or “In this work, we…”. -- Use active voice for the authors' choices, derivations, tests, and conclusions. -- Make the change visible in both structure and voice. Prior work may be described mostly in neutral or passive language where natural. The present contribution should switch clearly to active language using “we.” -- Do not hide this change in the middle of a paragraph. Start a new paragraph, subsection, or section when the scale of the paper permits it. -- Do not imply novelty merely by describing standard material in new terminology. - -A useful introduction progression is: - -1. Motivate the general problem. -2. Explain the established approaches relevant to that problem. -3. Identify the limitation or unresolved question. -4. State what this paper contributes. -5. Preview the principal result and its scope. - -Do not turn the contribution paragraph into a checklist of sentences beginning with “We present,” “We derive,” or “We demonstrate.” Group related contributions into a short argument. - -### Cite ideas rather than narrating authors - -Discuss the scientific concept or result and place the citation directly after it. Avoid humanities-style narration built around researchers' names. - -Prefer: - -> Rank-adaptive tensor-network integrators enlarge the represented space before truncation [7, 8]. - -Avoid: - -> Smith and Jones introduced a rank-adaptive tensor-network integrator [7]. - -Use author names only when the identity itself is relevant, such as a named theorem, a direct historical dispute, or wording that cannot otherwise be attributed clearly. A related-work section should compare assumptions, constructions, and results rather than recounting who did what. - -## 3. Match every claim to evidence - -For each central claim, ask: - -- What result supports it? -- Does that result establish the claim directly, or merely illustrate it? -- What alternative explanation has been ruled out? -- What qualification must remain? - -Use the weakest claim that communicates the actual result completely. - -Examples of important distinctions: - -- Numerical tests can validate an implementation or illustrate an identity. They do not prove a universal theorem. -- Decreasing error over a short timestep sequence shows empirical refinement. It does not establish a formal convergence order. -- One controlled example can identify a concrete failure mode. It does not show that the failure occurs for every model or implementation. -- Equal tolerances do not necessarily imply equal cost, equal discarded weight, or an equal-resource comparison. -- A method working after a correction does not imply that it outperforms established alternatives. -- Failure to observe an advantage is a result, not a reason to manufacture a stronger performance story. - -Preserve limitations wherever they affect interpretation. Simpler prose must not become stronger prose. - -## 4. Write for an informed non-specialist - -Assume the reader understands the general field but not this paper's particular construction. - -- Explain the problem before naming detailed mathematical objects. -- Introduce notation only when it becomes useful. -- Define each specialized term before relying on it. -- Prefer language already used in the closest literature. -- Keep established terminology when it is both precise and readable. Do not rename a known object merely to make the paper appear novel. -- Use an ordinary description instead of inventing a term for every implementation choice. -- Reserve dense terminology for sections where mathematical precision requires it. -- When two formulations are equally precise, choose the one that is more accessible. - -For example, first write “project the coefficients into the updated basis.” Introduce a shorter formal name only if the operation appears often enough that the name genuinely helps. - -A term should earn its place. Name a concept when the name improves later reasoning, not simply because the concept exists. - -## 5. Use simple, exact language - -### Sentence construction - -- Put one main idea in each sentence. -- Prefer concrete verbs such as “keep,” “remove,” “compare,” “project,” “increase,” and “compress.” -- Break up sentences containing several conditions, contrasts, or qualifications. -- Avoid noun stacks with several technical modifiers. -- Keep the subject and verb close together. -- State the result before discussing secondary details. -- Use “we” naturally, but not at the beginning of every sentence. - -### Words and structures to use sparingly - -- Flowery adjectives and promotional modifiers -- “Crucial,” “groundbreaking,” “robust,” “comprehensive,” and “systematic” unless they have a precise meaning -- “Highlights,” “underscores,” “reveals,” “offers insight into,” and “plays a pivotal role” when the result can be stated directly -- Repeated “not merely X, but Y” constructions -- Em dashes -- Heavy use of colons and semicolons -- Formal synonyms where an ordinary word is clearer -- Repeated three-part lists used only for rhetorical rhythm - -Replace claims about importance with the reason the result matters. Replace claims that a result “demonstrates” something with the observation and its supported interpretation. - -Avoid both extremes: prose should not be ornate, but it should not read like a sequence of clipped notes. Transitions should show how one question leads to the next. - -## 6. Build sections around reader questions - -At the beginning of each major section, state in one or two sentences why it is needed. Then move from that overview into the technical detail. At the end, return to the section's main answer and connect it to the next question without introducing unexplained material. - -Transitions must follow the reader's current knowledge. Do not write “as proved below,” interpret a result that has not yet been shown, or use terminology belonging to the next section. If later material must be previewed, describe only the motivating question in language already available to the reader. - -For each theoretical subsection, make clear: - -1. What problem is being formalized? -2. What assumptions are required? -3. What is proved? -4. Why is the result needed later? -5. What does the result not establish? - -For each numerical study, use this order: - -1. The question being tested -2. The construction of the test -3. The observed result -4. The conclusion supported by the observation -5. The conclusion that cannot be drawn - -Do not begin with a page of parameters before stating why the calculation exists. Give enough setup for reproducibility, then keep the main observation visible. - -## 7. Design the abstract as a compact argument - -The abstract should be understandable without the Methods section. A reliable structure is: - -1. The broad problem -2. The unresolved issue -3. The paper's approach -4. The central theoretical or methodological contribution -5. The decisive evidence -6. The main conclusion, including an important negative result if relevant - -Avoid: - -- notation; -- proposition or equation numbers; -- new terminology that is unnecessary to understand the result; -- a list of every experiment; -- claims of novelty or importance unsupported by the abstract itself; -- detailed implementation labels when a plain description works. - -The abstract should explain what happens and why it matters, not reproduce the manuscript's internal vocabulary. - -## 8. Keep the introduction progressive - -The introduction should follow the paper's V shape. It begins with the broad problem, narrows to the exact unresolved question, and ends by placing the contribution and main finding back in the wider context. - -It should not spoil results before the reader understands the problem, but it should still state the paper's main finding plainly. Preview the conclusion at a level appropriate for the introduction. Do not refer to a later proof, proposition number, figure, or technical mechanism before establishing the concepts needed to understand it. - -Each paragraph should perform one role: - -- establish the setting; -- narrow to the relevant methods; -- explain the unresolved problem; -- identify the contribution; -- summarize the evidence and conclusion. - -Do not introduce notation, algorithm labels, or fine distinctions before the reader needs them. Cite closely related work where the comparison becomes relevant, and state exactly how the present contribution differs. Frame these citations around the methods or findings rather than the researchers' names. - -## 9. Present methods in dependency order - -Definitions and operations should appear before anything that depends on them. - -- Begin with the minimum common notation. -- Explain each representation before manipulating it. -- Introduce the algorithm in the same order in which it operates. -- Separate exact mathematical statements from implementation conventions. -- Distinguish what holds before approximation or compression from what may fail afterward. -- State whether a choice is mathematically required, one valid implementation, or merely the convention used in the paper. - -If a passage cannot be simplified without losing precision, keep the formal language and add one plain explanatory sentence around it. - -## 10. Make results answer the paper's claims - -The Results section should be an evidence chain, not a log of completed computations. - -A useful progression for a methods paper is: - -1. Structural or unit-level checks of the derived properties -2. Independent correctness validation of the complete implementation -3. A controlled test isolating the proposed mechanism -4. Ablations ruling out plausible alternatives -5. A restrained comparison with established methods - -Report the computational environment and software used. Provide enough information to reproduce the study, including the relevant code revision, parameters, reference construction, and accuracy metrics. - -Use negative results directly. If a method is less accurate, slower, or less stable in the tested regime, state that result and adjust the paper's claim. Do not compensate with vague claims of potential superiority. - -## 11. Make figures and tables carry arguments - -Every figure or table should answer a question that matters to the storyline. - -Before adding one, ask: - -- What question does this item answer? -- Is a plot, table, or sentence the clearest format? -- What should the reader conclude from it? -- Is that conclusion stated in the surrounding text? - -Caption structure: - -1. A short bold title -2. What question is addressed or what is shown -3. The essential setup needed to interpret it -4. The main answer -5. Any qualification necessary to prevent overinterpretation - -Captions should be understandable on their own but should not reproduce an entire Results paragraph. Introduce every figure and table before interpreting it. Use the same terminology in the caption, legend, table entries, and main text. - -Use plots for trends, tradeoffs, or comparisons across several values. Use tables for exact values, configurations, or stage-by-stage states. Do not convert a table into a plot merely to make the paper look more visual. - -## 12. Use the discussion for interpretation - -Do not repeat the paper section by section. Answer: - -1. What was learned? -2. Why does it matter? -3. Which conclusions are limited to this setup? -4. What remains unresolved? - -Separate limitations caused by the implementation from limitations of the underlying method. A plausible but unsuccessful algorithmic choice is not automatically a coding error. - -End with a restrained statement of practical or scientific relevance. Do not advertise a general advance when the evidence establishes a focused one. - -## 13. Preserve the authors' voice - -Use prior papers by the same authors to calibrate paragraph length, level of explanation, and preferred transitions. Do not copy their wording or force the current paper into an unrelated template. - -Avoid repeated global rewrites. They tend to flatten the voice, introduce new terminology, and produce polished but generic prose. Once the structure is sound, prefer local edits made while reading the paper in order. - -Good scientific prose should sound like a knowledgeable author explaining a result carefully, not like a press release and not like an instruction manual. - -## 14. Editing workflow - -### Pass 1: Story and claims - -- Write the paper's story in plain language. -- List the central claims and the evidence supporting each one. -- Remove unsupported claims and disconnected results. -- Check novelty against the closest literature. - -### Pass 2: Structure - -- Reorder sections and paragraphs by conceptual dependency. -- Check the paper's V shape from broad motivation to specific evidence and back to broader interpretation. -- Apply the same overview-detail-transition pattern within major sections where it helps. -- Make the prior-work/new-work boundary explicit. -- Ensure that the shift to the present work is visible in both section structure and active voice. -- Ensure every section answers a necessary question. -- Remove forward references that require knowledge the reader does not yet have. -- Rewrite transitions so that each section motivates the next question without prematurely answering it. -- Move secondary derivations and diagnostics to appendices when appropriate. - -### Pass 3: Language - -- Remove filler, hype, and unnecessary adjectives. -- Simplify terminology and long sentences. -- Replace vague verbs with concrete statements. -- Improve transitions without adding rhetorical padding. - -### Pass 4: Evidence presentation - -- Check every figure, table, and caption against its reader question. -- Confirm that numerical comparisons use appropriate metrics and controls. -- Make negative results and limitations explicit. - -### Pass 5: Human read-through - -Read the manuscript from beginning to end without editing individual sentences on the first pass. Mark every point where the reader must stop, infer a missing connection, or remember an undefined term. Then repair those points locally. - -## 15. Non-negotiable editing constraints - -Unless an actual inconsistency is found, do not change: - -- equations; -- numerical values; -- citations; -- propositions or mathematical conditions; -- algorithm definitions; -- figure data; -- conclusions required by the evidence. - -Flag suspected inconsistencies instead of silently correcting them. Record any scientific changes separately from language edits. - -## 16. Final audit - -Read the paper in order as if unfamiliar with the work. Confirm that: - -- the complete story can be summarized in a short paragraph; -- every section advances that story; -- the paper narrows from general motivation to its specific contribution and widens again to interpretation; -- major sections open with an overview, provide the required detail, and close with an answer or bridge; -- every specialized term is explained before use; -- no paragraph depends on a later definition or result; -- no transition spoils a proof, result, or mechanism that the reader has not encountered; -- the boundary between known work and new work is obvious; -- the present contribution is marked by a clear structural and active-voice shift; -- literature is discussed through concepts and findings rather than author-name narration; -- each major claim has visible supporting evidence; -- limitations are stated where they affect interpretation; -- the abstract is understandable without the Methods; -- terminology is consistent across prose, equations, figures, and tables; -- captions state what is shown and what the reader should conclude; -- negative results have not been hidden or rhetorically softened; -- equations, references, numerical values, and cross-references remain intact; -- code and data availability statements describe what is actually accessible; -- the source compiles without new warnings or layout problems. - -## Recommended instruction for a writing assistant - -> Edit this manuscript for a coherent scientific storyline, precise claims, and natural technical prose. Write for a reader who understands the broader field but not this paper's particular construction. Use the simplest language that preserves the mathematics. First identify the paper's problem, gap, contribution, evidence, main conclusion, and limitations. Give the paper a V-shaped structure that moves from the broad problem to the specific contribution and evidence, then returns to the broader meaning and limitations. Use the same overview-detail-transition pattern within major sections where appropriate. Ensure that every section advances the storyline in the order the reader needs. Do not mention proofs, results, distinctions, or terminology before they have been introduced. Transitions should motivate the next question without spoiling its answer. Clearly separate established work from the present contribution through a noticeable structural change and a shift from mostly neutral or passive prose to active “we” language. Discuss previous literature through concepts and findings followed by citations, not through narration centered on author names. Define specialized terms before use, prefer established language from the closest literature, and avoid inventing labels that do not help later reasoning. Remove filler, promotional adjectives, em dashes, excessive colons or semicolons, repetitive rhetorical structures, and long jargon-heavy sentences. Preserve equations, numerical values, citations, propositions, and mathematical conditions unless an inconsistency is found; flag such inconsistencies rather than silently changing them. Match every claim to its evidence and retain all necessary qualifications. Organize each numerical study around its question, setup, observation, supported conclusion, and limitation. Make every figure and table answer a clear question, with a concise caption that states the main result. Finish with a complete read-through for logical dependencies, terminology, claim scope, compilation, and layout. Return the revised source with a short change log of substantive structural or scientific edits, not a catalogue of wording substitutions. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 8861bb881..000000000 --- a/TODO.md +++ /dev/null @@ -1,439 +0,0 @@ -# YAQS 1.0 Release TODO - -## Goal and scope - -Release the existing YAQS feature set with correct numerical behavior, a clear -stable API, and working documentation. Add no new features before 1.0. HDF5 -persistence is outside this release. - -Complete correctness repairs and validation first. Then Aaron reviews the code -and reads and updates the README and documentation. Resolve findings from those -reviews before validating and publishing the release candidate. - -Chunks 1 through 5 contain the required software-release work. Optional cleanup -and SciPost paper work have separate sections below. - -## Current status - -The review on 2026-10-07 covered local `remove-legacy` at `9b066659` and GitHub -`main` at `e22c3ef4`. The local branch was merged through PR #609; later changes -on `main` include dependency and tooling updates. - -- [x] Establish one public spatial ordering: site 0 is the least-significant - subsystem. Cover asymmetric states, operators, local noise, observables, - mixed physical dimensions, and memory-characterization conversions. -- [x] Use uncapped direct process-tensor construction by default. Warn that - finite branch caps are experimental and document exponential cost. -- [x] Validate process-tensor shape, conditional-state positivity, Bloch bounds, - and information metrics. Cover dense and analytic references. -- [x] Harden public simulation controls, mutable time grids, Hamiltonian inputs, - real-valued results, state inputs, tensor shapes, and cross-object - physical dimensions. Test public errors under normal Python and - `python -O`. -- [x] Reject unsupported dense-representation diagnostics and multi-time output - combinations. Preserve sampled and final Schmidt spectra for MPS runs, - including noisy trajectories and direct MPS spectrum evaluation. -- [x] Remove legacy solver fallbacks and preserve explicit backend selection. -- [x] Separate local observables from gate classes and support explicit - final-state full-chain expectations through `MPS.expect_mpo()`. -- [x] Pin the current top-level exports with `tests/test_public_api.py`. -- [x] Make core-only imports silent and include `py.typed` in the wheel. -- [x] Add serial/parallel reproducibility, explicit process-pool, spawn-process, - and JIT-enabled tests. Keep slow rate recovery in the manual release tier. -- [x] Pass the local Python 3.14 serial suite: 3,288 passed, 3 skipped, 3 - deselected, and 5 xfailed. Pass the manual rate-recovery test separately. -- [x] Pass current-main CI on Ubuntu, Ubuntu ARM, macOS, and Windows, including - Linux and Windows JIT checks and the clean-wheel check. -- [x] Build and inspect the current-main sdist and wheel. - -These checks apply to the named commits. They do not replace validation of the -final candidate. Strict documentation has known content defects. Fresh clean -notebook execution and external link checking remain release requirements. - -## Working rules - -- Keep each repair small enough for a focused review. -- Add or update behavioral regressions for every code change in the test tree - owned by the affected component. Test the supported public contract. -- Run targeted tests during development and `uvx nox -s lint` after each batch - of changes. Resolve substantive failures before moving to the next chunk. -- Update `CHANGELOG.md` and `UPGRADING.md` for user-facing or breaking changes. - Include required PR references and author links, and disclose AI assistance in - any authorized pull request. -- Preserve independent numerical references and slow scientific regressions. - Remove only genuine duplication or unnecessary cost. -- Preserve Python 3.11 through 3.14 testing on every supported CI platform. -- Fix public-boundary validation and numerical defects without adding repeated - whole-network checks to numerical loops or rewriting the package structure. -- Keep unsupported, approximate, and experimental combinations explicit. -- Do not modify template-managed files directly. Address template changes - upstream or state package-specific behavior in repository-owned documentation. - -## Chunk 1: Correctness repairs and validation - -Complete this chunk before Aaron's code and documentation reviews. - -### 1.1 Make Schmidt-spectrum output agree with the supported contract - -Spectrum observables retain trajectory, sample, and coefficient axes. MPS analog -and digital runs, deterministic list ensembles, and simulation programs support -sampled and final-only spectra, including noisy pure trajectories. - -- [x] Define result shapes: `(num_traj, num_samples, 500)` for trajectories and - `(num_samples, 500)` for means. Preserve descending coefficients and `NaN` - padding from direct `MPS.get_schmidt_spectrum()`. -- [x] Repair worker buffers and result storage without changing scalar result - shapes or observable ordering. -- [x] Average trajectory coefficients with missing ranks counted as zero. - Preserve padding when every trajectory lacks a coefficient. Explain that - this mean is not a Schmidt spectrum of the mixed state. -- [x] Stitch program mean spectra along the sample axis and retain individual - trajectories in segment results. -- [x] Cover both analog orders, digital checkpoints, changing ranks, mixed - observable ordering, sampled and final-only output, deterministic - ensembles, and noisy serial/parallel execution. Preserve direct MPS tests - and independent dense SVD and analytic trajectory references. -- [x] Update observable and result documentation, examples, and release notes. - Remove the obsolete spectrum-concatenation branch and its tests. - -Acceptance: scheduled spectra agree with independent references, preserve their -axes across execution paths, and work for noisy trajectories without requesting -a representative final state. - -### 1.2 Honor noise-optimizer controls and report losses accurately - -Noise fitting applies the validated optimizer limits and reports the loss of the -supplied initial model separately from candidate history. - -- [x] Pass `max_iter` to bounded scalar search. Document CMA-ES generations, - SciPy's evaluation stopping limit, and the two-evaluation startup case. -- [x] Evaluate the initial model before optimization and store `initial_loss`. - Make `sqrt_loss_before()` use this baseline. Keep candidate history - separate from baseline and final-fit evaluations. -- [x] Add public characterization regressions with one and two fitted parameters - and small iteration limits. Cover scalar and CMA-ES dispatch. -- [x] Compare the baseline with independent analytic Pauli-noise trajectories. - Preserve fitted-rate and dynamics recovery coverage. -- [x] Update result and optimizer docstrings, the digital-twin example, and - release notes. - -Acceptance: optimizer controls affect execution, and the before-optimization -loss describes the supplied initial model. Fitted-rate recovery still passes. - -### 1.3 Honor quiet simulation during shot readout - -Simulator progress controls cover trajectory and shot-readout bars, including -program segments. Progress remains visible by default. Simulator shot readout -runs within each trajectory and does not create another process pool. - -- [x] Make shot readout respect `show_progress=False` through `Simulator.run`. -- [x] Document execution controls: `parallel` and `max_workers` govern - trajectory pools, and simulator shots run serially within each trajectory. - Direct `MPS.measure_shots()` retains parallel sampling. -- [x] Remove nested shot pools from combined noisy observable-and-shot runs. - `parallel=False` keeps simulator readout in the current process. -- [x] Add regressions for quiet and visible readout, direct MPS sampling, - program segments, worker limits, and shot counts. Preserve seeded - trajectory tests. - -Acceptance: documentation runs can suppress all progress bars, default progress -remains visible, and shot readout cannot bypass simulator worker controls. - -### 1.4 Verify existing workflows after the repairs - -Use existing tests and references first. Add tests for uncovered supported -contracts or concrete regressions, not for a new feature matrix. - -- [x] Run targeted regressions for the repairs above and public validation. -- [x] Check asymmetric analog evolution across MPS/TJM, vector/MCWF, and - density-matrix/Lindblad paths against independent dense references. -- [x] Check noisy analog dynamics against analytic or Lindblad references. - Preserve jump-probability and trajectory-convergence checks. -- [x] Check default-MPO digital evolution against Qiskit, including long-range - and multi-qubit gates, observable ordering, and shot counts. -- [x] Check list ensembles, piecewise evolution, and mixed programs against - existing independent or exact small-system references. -- [x] Check memory characterization, default direct process tensors, conditional - responses, information metrics, and noise fitting against existing - references. -- [x] Re-run the full serial suite with numerical-library thread limits. -- [x] Pass the supported OS/Python CI matrix and minimum-dependency tests. - Review warnings from minimum-dependency runs, not only their exit status. -- [x] Pass Linux and Windows JIT tests with compilation enabled and no coverage. -- [x] Pass `uvx nox -s release-tests` and `uvx nox -s lint`. -- [x] Record commit, environment, commands, outcomes, and accepted limitations. - Account for every skip and xfail. The five circuit-TDVP rank-growth xfails - may remain only with accurate documentation and a supported default route. -- [x] Replace automatic Linux `fork` with `forkserver`. Preserve explicit start - methods and real process-pool coverage. - -Acceptance: no advertised stable workflow has an unresolved failure. Relevant -independent references support numerical agreement. - -Validation on 2026-10-08 covers `pool-fix` at `7d1e5c3a` on Linux with Python -3.14.2. The full serial suite passed with 3,378 passed, 3 skipped, 3 deselected, -and 5 xfailed in 135.58 seconds. The skips are invalid site/chain combinations. -The five xfails cover documented rank-growth limits in the optional circuit-TDVP -paths; the default MPO route passes. The three deselected tests passed -separately: one manual rate-recovery test and two JIT tests. Lint passed. - -The serial run used numerical thread limits of one, `YAQS_MAX_WORKERS=2`, -`NUMBA_DISABLE_JIT=0`, and pytest -`-n 0 -p no:cacheprovider -m 'not release and not jit' --durations=30`. -Dedicated integration tests retain explicit process pools. Nox ran -`release-tests` and `jit-tests` without coverage. Environment versions, -commands, XML results, logs, and accepted limitations are recorded in -`/tmp/yaqs-1-4-validation-7d1e5c3a/`. - -[CI run 37695876971](https://github.com/munich-quantum-toolkit/yaqs/actions/runs/37695876971) -passed for the same commit. Python 3.11 through 3.14 passed on Ubuntu x86, -Ubuntu ARM, macOS, and Windows. Both Ubuntu minimum-dependency runs passed, as -did Linux and Windows JIT tests and the Linux wheel check. - -The earlier Python 3.14 minimum-dependency CI run emitted 14 warnings about -`fork` from multi-threaded processes. Automatic Linux context selection now uses -`forkserver`. Explicit start methods remain supported. - -Local follow-up validation on 2026-10-08 covers `7d1e5c3a` plus the working-tree -repair. The full Python 3.14 suite passed with 3,382 passed, 3 skipped, 3 -deselected, and 5 xfailed in 141.86 seconds. The minimum-dependency suite passed -with 3,382 passed, 3 skipped, and 5 xfailed in 46.00 seconds. Both runs treated -`multiprocessing.popen_fork` deprecation warnings as errors; neither reported -fork warnings. Minimum dependencies include Numba 0.63.0, NumPy 2.3.2, SciPy -1.16.1, and Torch 2.9.0 CPU. All optional tests remain included. - -The 36 targeted checks passed on Python 3.11 and 3.14. They include a live -parent thread with two real workers, serial/parallel shot counts, seeded -noise-fitting trajectories, and automatic and explicit-spawn equivalence -checking. Lint passed. Commands, source checksums, environments, timings, and -logs are in `/tmp/yaqs-forkserver-validation-7d1e5c3a/`. The supported OS/Python -CI matrix must validate the updated branch before merge. - -### 1.5 Continue trajectory RNG streams across MCWF memory segments - -MCWF memory characterization uses one continuous RNG stream per trajectory -across evolution segments. Independent noisy-channel references protect -conditional process-tensor responses and joint weights. - -- [x] Let consecutive MCWF segments reuse the worker-owned trajectory RNG. -- [x] Forward that RNG through the memory-characterization backend. -- [x] Compare noisy dense process-tensor conditional responses and joint weights - with an analytic channel across two nonzero evolution slots. -- [x] Preserve single-segment reproducibility and the existing TJM RNG contract. - -Acceptance: seeded MCWF process-tensor predictions agree with independent noisy -channel references within justified sampling error. - -## Chunk 2: Aaron's code review and API freeze - -Start after chunk 1 passes. Review substantive correctness and maintainability. -Avoid broad refactoring for appearance or module size. - -- [ ] Review simulator dispatch, state handoff, parameter mutation, result - allocation, time grids, observable ordering, and output aggregation. -- [ ] Review MPS/MPO conversions, site ordering, physical dimensions, - orthogonality-center tracking, normalization, and truncation contracts. -- [ ] Review analog integration, dissipation, jump selection, and supported - MCWF, Lindblad, ensemble, and piecewise paths. -- [ ] Review digital gates, long-range operations, measurement, noise - application, and equivalence-checking results and approximation claims. -- [ ] Review memory and noise characterization, process-tensor physicality, - conditional responses, information metrics, and optimizer contracts. -- [ ] Review public validation, exception behavior, RNG streams, optional - imports, parallel execution, and optimized-Python behavior. -- [ ] Review automated coverage against these contracts. Preserve independent - references and distinguish unit tests from scientific reproduction. -- [ ] Record and resolve major findings with focused tests. Re-run affected - checks before accepting each repair. -- [ ] Define stable import paths for existing facades, result types, and - process-tensor types. Use existing paths where practical; do not expand - the public surface merely to flatten the package. -- [ ] Pin the final supported boundary in public API tests. Keep workers, - encoders, backend helpers, and other implementation details outside it. -- [ ] Confirm that supported combinations work or have clear errors, and every - approximation or experimental path has a stated limitation. -- [ ] Complete human review of the code and materially AI-assisted changes. - -Acceptance: Aaron understands the major numerical and public contracts and has -no unresolved major finding. The stable API can carry compatibility promises -throughout 1.x. - -## Chunk 3: Aaron's README and documentation review - -### 3.1 Read and update the user-facing text - -- [ ] Read and update the README: installation, first-use examples, feature - scope, limitations, support, and software and method citations. -- [ ] Read the installation and all user guides. Check each guide against the - final implementation and supported imports. -- [ ] Review the API reference for accurate signatures, result fields, return - types, defaults, units, ordering, shapes, and error behavior. -- [ ] Add one supported-combinations table for analog representations, digital - MPS simulation, list ensembles, piecewise and mixed programs, local and - bitstring observables, shot readout, and characterization backends. -- [ ] State process-tensor exponential cost, operations that densify, finite-cap - experimental status, and circuit-TDVP rank-growth limits. Keep the default - circuit MPO route and explicit `MPS.expect_mpo()` contractions clear. -- [ ] Correct `UPGRADING.md`: `Observable` exposes `.type`, not `.kind`. -- [ ] Document existing `Result` fields, requested versus executed trajectory - counts, time and observable axes, shot totals, diagnostics, final states, - multi-time outputs, and nested program results. Add no result API - features. -- [ ] Describe pickle as trusted, same-version, temporary checkpoint storage. Do - not promise portable or versioned result persistence. -- [ ] State the tested Python range as 3.11 through 3.14 and confirm support - ownership and the maintenance policy. -- [ ] Use consistent terms and precise prose. Separate method-paper citations - from the software citation and bound numerical claims by their evidence. -- [ ] Publish existing evidence for the claimed equivalence-checking eight-qubit - crossover, including the referenced benchmark script, or remove the claim. - Describe the configured automatic cutoff as a heuristic where appropriate. -- [ ] Read and finalize release notes for all user-facing and breaking changes, - with PR references and all contributing authors. - -### 3.2 Verify examples and the documentation build - -- [ ] Use supported imports in first-use and ordinary user examples. Mark any - intentionally documented low-level API clearly. -- [ ] Give stochastic examples fixed seeds where reproducibility matters. Use - documented valid seeds, including zero where supported. -- [ ] Add tolerances and independent reference assertions to the small - representation-comparison example. Verify other release examples' expected - outputs without duplicating the numerical test suite. -- [ ] Fix the unknown Mermaid directive in `docs/index.md` and malformed - bibliography directive in `docs/references.md`. -- [ ] Fix document, method, citation, and included-file references, including - first-use links and the `State.from_mps` reference. -- [ ] Curate AutoAPI to remove duplicate objects and unresolved targets and - distinguish stable API from implementation details. -- [ ] Add a fast strict documentation check to CI. Pass a fresh - `sphinx-build -E -a -n -T -W --keep-going` build without blanket warning - suppression. Keep necessary external-reference exceptions narrow. -- [ ] Execute every documentation notebook and README workflow in a clean - environment using a wheel built from the reviewed checkout. Cover optional - examples with declared extras and keep the source tree off the import - path. Repeat these checks on the tagged release candidate in chunk 5. -- [ ] Run `uvx nox -s docs -- -b linkcheck`. Resolve broken project links and - record necessary exceptions for unavailable external sites. -- [ ] Inspect rendered desktop and narrow-screen pages: code blocks, diagrams, - figures, tables, navigation, API links, and included release notes. -- [ ] Confirm that Read the Docs builds the reviewed documentation successfully. -- [ ] Complete Aaron's own README and documentation review and resolve findings. - -## Chunk 4: Compatibility policy and release metadata - -- [ ] Commit to compatibility for the documented stable API throughout 1.x. - Remove the changelog exception permitting breaking minor releases, or - limit that exception explicitly to pre-1.0 versions. -- [ ] Finalize the 1.0 changelog and migration instructions from the last - release. -- [ ] Change the final package development-status classifier from Beta to the - appropriate stable-release status. -- [ ] Add `CITATION.cff` for the software release and align citation - instructions. -- [ ] Verify authors, maintainers, license, supported Python versions, extras, - package metadata, and repository, changelog, and release URLs. -- [ ] Plan the software archive and DOI or release identifier. Add the final - identifier to citation and release metadata when it becomes available. -- [ ] Review GitHub issues against the frozen scope. Record that issue #416's - original gate-coupling problem is resolved; keep issue #35 and other - new-feature requests outside the 1.0 milestone. -- [ ] Require optional runner upgrades or other infrastructure changes only when - they fix a demonstrated release failure. - -## Chunk 5: Release candidate and final gate - -Mark these complete for the exact candidate, even where an earlier commit passed -an equivalent check. Re-run affected checks after any candidate change. - -- [ ] Publish or distribute `1.0.0rc1` after correctness and human reviews pass. -- [ ] Build the sdist and wheel from the candidate tag with its actual version. -- [ ] Inspect artifact contents: version, public modules, `py.typed`, license, - metadata, and required source-distribution files. -- [ ] Install and exercise the wheel and sdist in clean environments. Verify a - silent core-only import and documented workflows outside the source tree. -- [ ] Run current dependencies on Python 3.11, 3.12, 3.13, and 3.14 on Ubuntu, - Ubuntu ARM, macOS, and Windows. Preserve the current coverage policy. -- [ ] Run minimum dependencies on Ubuntu with Python 3.11 and 3.14. -- [ ] Pass JIT-enabled checks on Ubuntu and Windows, optional-dependency checks, - the clean-wheel check, and the manual scientific release tests. -- [ ] Run the full serial suite and `uvx nox -s lint` on the final source. -- [ ] Pass strict documentation, installed-wheel examples, all executable - notebooks, external link checking, and Read the Docs. -- [ ] Review remaining skips, xfails, warnings, approximations, and experimental - paths. Confirm their technical reasons and accurate user documentation. -- [ ] Record source commit and tag, dependency versions, commands, test and - documentation results, and artifact checksums in the release evidence. -- [ ] Complete Aaron's final review of artifacts and user-facing material. - Resolve every remaining software release blocker. -- [ ] Tag and publish 1.0, archive the exact published source and artifacts, and - complete citation metadata with the archive identifier. - -For memory and process-tensor checks, use serial execution and capped numerical -threads when needed: - -```bash -OPENBLAS_NUM_THREADS=1 \ -OMP_NUM_THREADS=1 \ -MKL_NUM_THREADS=1 \ -NUMEXPR_NUM_THREADS=1 \ -NUMBA_NUM_THREADS=1 \ -uv run pytest -n 0 -p no:cacheprovider tests/characterization/memory tests/test_memory_characterizer.py -``` - -## Optional cleanup - -These tasks do not block 1.0 unless they expose a correctness, reliability, or -resource-use defect. - -- [x] Consolidate duplicate analog and digital golden tests while preserving - independent references, observable ordering, and actual pool coverage. -- [x] Reduce redundant shot sampling or flaky random assertions using tests of - the intended probability or measurement contract. -- [ ] Shorten the quickstart and move advanced material into focused guides - where that improves first use. -- [ ] Reduce optional documentation dependency cost if the build is needlessly - expensive. Preserve coverage of supported optional paths. -- [x] Record slowest-test durations and investigate avoidable memory use. Keep - scientific regression tests whose cost protects meaningful behavior. - -## SciPost paper evidence - -These tasks are required for the paper and its numerical claims. They do not -block the software release unless the release makes the same unsupported claim. -Archive data without adding package persistence or uncertainty APIs. - -- [ ] Select paper examples, benchmarks, figures, and claims supported by the - frozen existing feature set. -- [ ] Archive scripts, inputs, probe settings, intervention schedules, seeds, - raw outputs, and plotting scripts for every quantitative claim. -- [ ] Record the YAQS commit and tag, dependency lock or container, Python - version, OS, hardware, commands, metrics, and tolerances. -- [ ] Add checksums for each paper input, result, and figure. Store numerical - arrays with readable metadata rather than relying on saved Python objects. -- [ ] Reproduce every paper figure and numerical table in a clean environment - using archived inputs and the published software artifact. -- [ ] Report actual trajectory counts and statistical errors from archived raw - trajectories in the paper analysis. -- [ ] Complete and archive the response-matrix campaign if the paper uses it. - Keep unresolved scientific discrepancies explicit. -- [ ] Finalize the paper's software citation and archive links separately from - citations to the underlying methods. - -## Deferred features and refactoring - -The following work is outside 1.0: - -- HDF5 persistence, a versioned result-file schema, or a new save/load API. -- Early stopping for stochastic trajectories. -- A circuit statevector backend. -- Neural-network noise characterization. -- A new process-tensor compression algorithm. -- Scheduled or trajectory-averaged `Observable(MPO)` support. -- Converting local observables, diagnostics, or bitstrings into MPOs. -- A new uncertainty-reporting `Result` API. -- Multi-level or qudit simulation features. -- Trajectory visualization. -- Broad rewrites based only on module size and a package-wide validation - reorganization. -- Compatibility adapters for abandoned pre-1.0 APIs. From 6d18364419aa12c09c9fee730240d668777c27a8 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:24:26 +0200 Subject: [PATCH 26/30] Fix repository links in included release notes --- docs/UPGRADING.md | 2 +- docs/conf.py | 20 +++++++++++++++++ tests/docs/test_build.py | 46 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index a005cf7a2..6794a0659 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -1,3 +1,3 @@ ```{include} ../UPGRADING.md - +:relative-docs: docs/ ``` diff --git a/docs/conf.py b/docs/conf.py index 014288c4a..9c6ed9d13 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,6 +10,7 @@ from __future__ import annotations import os +import re import sys from importlib import metadata from pathlib import Path @@ -22,6 +23,7 @@ if TYPE_CHECKING: from pybtex.database import Entry from pybtex.richtext import HRef + from sphinx.application import Sphinx ROOT = Path(__file__).parent.parent.resolve() sys.path.insert(0, str(Path(__file__).parent / "_ext")) @@ -204,3 +206,21 @@ def format_url(self, _e: Entry) -> HRef: # ruff:ignore[no-self-use] "source_directory": "docs/", "navigation_with_keys": True, } + + +def _release_source_links(app: Sphinx, relative_path: Path, parent_docname: str, content: list[str]) -> None: + """Link repository source files from included release notes to GitHub.""" + del parent_docname + if relative_path.as_posix() not in {"../CHANGELOG.md", "../UPGRADING.md"}: + return + repository = Path(app.srcdir).parent + for target in re.findall(r"\]\((src/[^)\s]+)\)", content[0]): + if (repository / target).is_file(): + content[0] = content[0].replace( + f"]({target})", f"](https://github.com/munich-quantum-toolkit/yaqs/blob/main/{target})" + ) + + +def setup(app: Sphinx) -> None: + """Preserve repository links when release notes are included in the docs.""" + app.connect("include-read", _release_source_links) diff --git a/tests/docs/test_build.py b/tests/docs/test_build.py index 6bc4f82a2..f81679faf 100644 --- a/tests/docs/test_build.py +++ b/tests/docs/test_build.py @@ -44,6 +44,52 @@ def _write_configuration(source: Path, extensions: list[str], extra: str = "") - ) +@pytest.mark.parametrize("missing_source", [False, True]) +def test_included_release_links(tmp_path: Path, *, missing_source: bool) -> None: + """Included release notes link to source and guides without changing the originals.""" + pytest.importorskip("sphinx") + pytest.importorskip("myst_nb") + pytest.importorskip("pybtex") + source = tmp_path / "docs" + source.mkdir() + _write_configuration(source, ["myst_nb"], 'nb_execution_mode = "off"\n') + module = tmp_path / "src" / "example.py" + module.parent.mkdir() + if not missing_source: + module.write_text('"""Example source."""\n') + releases = { + "CHANGELOG.md": "# Changelog\n\nSee [example](src/example.py).\n", + "UPGRADING.md": "# Upgrading\n\nSee [guide](docs/guide.md#time-dependent-hamiltonians).\n", + } + for name, text in releases.items(): + (tmp_path / name).write_text(text) + shutil.copyfile(Path(__file__).parents[2] / "docs" / name, source / name) + (source / "index.md").write_text("# Release documentation\n\n```{toctree}\nCHANGELOG\nUPGRADING\nguide\n```\n") + (source / "guide.md").write_text("# Guide\n\n## Time-dependent Hamiltonians\n") + output = tmp_path / "html" + completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - sys.executable is trusted. + [sys.executable, "-m", "sphinx", "-W", "-T", "-b", "html", str(source), str(output)], + check=False, + capture_output=True, + text=True, + timeout=60, + ) + log = completed.stdout + completed.stderr + for name, text in releases.items(): + assert (tmp_path / name).read_text() == text + if missing_source: + assert completed.returncode != 0 + assert "cross-reference target not found: 'src/example.py'" in log + return + assert completed.returncode == 0, log + assert "WARNING:" not in log + assert ( + 'href="https://github.com/munich-quantum-toolkit/yaqs/blob/main/src/example.py"' + in (output / "CHANGELOG.html").read_text() + ) + assert 'href="guide.html#time-dependent-hamiltonians"' in (output / "UPGRADING.html").read_text() + + def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: """Both documentation formats retain outputs from one capped notebook run.""" pytest.importorskip("sphinx") From c9cd6b847f2f789aaebdf28cdd3d8f8a63aa376a Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:04:41 +0200 Subject: [PATCH 27/30] Bound Read the Docs notebook execution to one Sphinx worker --- .readthedocs.yaml | 7 +++++++ tests/docs/test_build.py | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 2e28b0866..95b988988 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,6 +11,13 @@ build: jobs: install: - uv pip install --python "$READTHEDOCS_VIRTUALENV_PATH/bin/python" --group docs --torch-backend cpu --exact -e '.[qasm3,torch]' + build: + html: + # Execute one notebook at a time; each simulation has its own worker budget. + - >- + "$READTHEDOCS_VIRTUALENV_PATH/bin/python" -u -m sphinx + -j 1 -n -T -W --keep-going -b html -d docs/_build/doctrees + docs "$READTHEDOCS_OUTPUT/html" python: install: diff --git a/tests/docs/test_build.py b/tests/docs/test_build.py index f81679faf..0c3370ca5 100644 --- a/tests/docs/test_build.py +++ b/tests/docs/test_build.py @@ -12,11 +12,13 @@ import json import os import re +import shlex import shutil import subprocess import sys import zlib from pathlib import Path +from string import Template import pytest @@ -96,6 +98,7 @@ def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: pytest.importorskip("myst_nb") pytest.importorskip("sphinx_llm.txt") pytest.importorskip("pybtex") + yaml = pytest.importorskip("yaml") source = tmp_path / "docs" source.mkdir() @@ -144,8 +147,13 @@ def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: environment.pop(name, None) environment["IPYTHONDIR"] = str(tmp_path / ".ipython") environment["JUPYTER_RUNTIME_DIR"] = str(tmp_path / ".jupyter") + environment["READTHEDOCS_VIRTUALENV_PATH"] = sys.prefix + environment["READTHEDOCS_OUTPUT"] = str(output.parent) + configuration = yaml.safe_load((Path(__file__).parents[2] / ".readthedocs.yaml").read_text()) + command = configuration["build"]["jobs"]["build"]["html"][0] completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - sys.executable is trusted. - [sys.executable, "-m", "sphinx", "-W", "-T", "-b", "html", str(source), str(output)], + [Template(argument).substitute(environment) for argument in shlex.split(command)], + cwd=tmp_path, env=environment, check=False, capture_output=True, From d39eee478ae958b24f396eb597aa3debef1844cd Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:32:39 +0200 Subject: [PATCH 28/30] shortened surrogate to reduce runtime --- docs/examples/memory_surrogate.md | 393 +++++++++--------------------- docs/examples/quickstart.md | 126 +--------- docs/index.md | 2 +- 3 files changed, 122 insertions(+), 399 deletions(-) diff --git a/docs/examples/memory_surrogate.md b/docs/examples/memory_surrogate.md index 6089ebbb0..943b26195 100644 --- a/docs/examples/memory_surrogate.md +++ b/docs/examples/memory_surrogate.md @@ -6,47 +6,33 @@ language_info: name: python mystnb: number_source_lines: true - execution_timeout: 900 + execution_timeout: 120 --- # Predicting Non-Markovian Dynamics A control pulse changes a quantum system and its later interaction with the -environment. Predicting that response usually requires evolving the joint system -and environment again for each control sequence. A surrogate learns from -simulated sequences so that we can query new controls without repeating that -evolution. - -Here we train on random controls, then predict how a chosen rotation changes the -final coherence of a probe qubit. We extend {doc}`quickstart` by comparing two -environment couplings and checking each prediction against direct Hamiltonian -evolution. Two qubits keep the reference calculation small; this example teaches -the workflow rather than demonstrating a speed advantage. +environment. A surrogate learns that response from simulated sequences, then +predicts reduced system states for new controls. Here we train one small model +and compare a pulse-angle sweep with exact two-qubit evolution. ```{note} **Experimental feature.** Surrogate modeling is not yet supported by a published -YAQS paper. Validate predictions for your controls and time horizon against -reference simulations or measurements. This example uses two interventions; it -does not establish reliable prediction for long control protocols. +YAQS paper. This small example teaches the workflow; it does not demonstrate a +speed advantage or establish accuracy for longer control sequences. Validate +predictions against simulations or measurements for your intended use. ``` -Install the PyTorch extra with `uv pip install "mqt.yaqs[torch]"`. The example -also uses Matplotlib. Run the cells in order in a notebook; for a script, use -the entry-point guard in {doc}`simulator_initialization`. - -## 1. Choose the system and control times +Install the PyTorch extra with `uv pip install "mqt.yaqs[torch]"`. The plot also +uses Matplotlib. Run the cells in order in a notebook; for a script, use the +entry-point guard in {doc}`simulator_initialization`. -Site 0 is the probe, and site 1 is an unobserved environment qubit. Their -Hamiltonian is +## 1. Choose the system and train on random controls -$$ -H=-JZ_0Z_1-g(X_0+X_1), \qquad g=0.5. -$$ - -The environment starts in $|0\rangle$. During training we vary the probe -preparation and apply random single-qubit rotations to the probe. The joint -state evolves between rotations, so the environment can retain information about -earlier controls. +Site 0 is the probe, and site 1 is an environment qubit initially in +$|0\rangle$. Their Hamiltonian is $H=-Z_0Z_1-0.5(X_0+X_1)$, with $\hbar=1$. We +apply two random single-qubit rotations to the probe, each followed by evolution +for $0.6$. The environment can retain information about the earlier control. ```{code-cell} python import numpy as np @@ -54,76 +40,44 @@ import torch from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer -num_steps = 2 interval = 0.6 schedule = [0.0, interval, interval] -couplings = [0.3, 1.0] -field = 0.5 -hamiltonians = {coupling: Hamiltonian.ising(2, J=coupling, g=field) for coupling in couplings} +hamiltonian = Hamiltonian.ising(2, J=1.0, g=0.5) params = AnalogSimParams(elapsed_time=interval, dt=interval, preset="fast") characterizer = MemoryCharacterizer(show_progress=False) -``` - -`timesteps` contains one more duration than there are interventions. The initial -`0.0` means that the first intervention occurs immediately after preparation. -Evolution for $0.6$ follows each intervention, giving a final time of $1.2$ in -units with $\hbar=1$. These durations become part of the training problem; -`predict` does not accept a new time grid. - -We use the same schedule at weak coupling, $J=0.3$, and stronger coupling, -$J=1$. Each Hamiltonian needs its own model. Coupling strength is not an input -to the trained surrogate. The public training path also does not accept a -`NoiseModel`; this comparison changes environmental coupling rather than adding -a Lindblad noise channel. - -## 2. Train on random control sequences -`sample` generates a validation dataset. `train` generates a separate training -dataset and fits the surrogate. Setting `intervention_style="haar"` draws random -single-qubit unitaries at both control times. The default initialization samples -a pure probe state from each random density matrix's eigenstates; the -environment remains in $|0\rangle$. - -```{code-cell} python -models = {} -for coupling, hamiltonian in hamiltonians.items(): - torch.manual_seed(7) - validation = characterizer.sample( - hamiltonian, params, num_interventions=num_steps, n=256, seed=99, - timesteps=schedule, intervention_style="haar", - ) - models[coupling] = characterizer.train( - hamiltonian, params, num_interventions=num_steps, n=4096, seed=7, - timesteps=schedule, intervention_style="haar", - model_kwargs={"d_model": 64, "num_layers": 2, "dim_ff": 128}, - train_kwargs={"epochs": 400, "lr": 1e-3, "device": "cpu", "val_dataset": validation}, - ) +torch.manual_seed(7) +validation = characterizer.sample( + hamiltonian, params, num_interventions=2, n=128, seed=99, + timesteps=schedule, intervention_style="haar", +) +model = characterizer.train( + hamiltonian, params, num_interventions=2, n=2048, seed=7, + timesteps=schedule, intervention_style="haar", + model_kwargs={"d_model": 64, "num_layers": 2, "dim_ff": 128}, + train_kwargs={"epochs": 200, "lr": 1e-3, "device": "cpu", "val_dataset": validation}, +) ``` -The seeds separate training and validation sequences. At the end of training, -YAQS restores the model with the lowest validation loss across the 400 epochs. -The validation set therefore selects the model; it is not an independent test of -the predictions below. The small architecture and CPU setting bound this -example's training cost. Other hardware, seeds, and PyTorch versions can give -different errors. +`timesteps` contains the initial delay and one duration after each intervention. +The initial `0.0` places the first rotation immediately after preparation, and +the final time is $1.2$. Training varies the probe preparation and draws random +unitaries with `intervention_style="haar"`; the environment preparation stays +fixed. YAQS restores the model with the lowest validation loss. The validation +set selects the model, so it is not an independent accuracy test. -The resulting model learns a mapping from the initial probe state and control -sequence to reduced probe states. It does not reconstruct the environment's -state. Both the environment preparation and the two-intervention horizon stay -fixed throughout this example. +## 2. Predict a pulse-angle sweep -## 3. Predict the response to a chosen pulse - -Prepare the probe in $|+\rangle=(|0\rangle+|1\rangle)/\sqrt{2}$. Apply the -identity at the first control time, let the joint system evolve for $0.6$, then -apply $R_z(\theta)$ to the probe. The surrogate predicts its state after the -second evolution interval. We sweep the angle while using the same model. +Prepare the probe in $|+\rangle$. Use the identity as the first control, then +apply $R_z(\theta)$ between the two evolution intervals. These chosen controls +were not supplied during training. At $\theta=0$ the system evolves freely; +$\theta=\pi$ gives a phase flip halfway through. ```{code-cell} python plus = np.array([1, 1], dtype=complex) / np.sqrt(2) rho0 = np.outer(plus, plus.conj()) identity = np.eye(2, dtype=complex) -pulse_angles = np.linspace(0, 2 * np.pi, 61) +pulse_angles = np.linspace(0, 2 * np.pi, 41) sequences = [ [ {"unitary": identity}, @@ -131,219 +85,104 @@ sequences = [ ] for angle in pulse_angles ] -predictions = { - coupling: np.stack([characterizer.predict(model, rho0, sequence) for sequence in sequences]) - for coupling, model in models.items() -} -``` - -`predict` returns a complex array of shape `(2, 2)`. Each stacked sweep has -shape `(61, 2, 2)`, with the first axis following `pulse_angles`. These chosen -sequences were not supplied during training; the model must generalize from its -random controls. At $\theta=0$ the sequence gives free evolution, and -$\theta=\pi$ gives a phase flip halfway through the evolution. - -The following figure uses the stronger coupling. Its left panel projects the -final states onto the equatorial Bloch plane, with coordinates -$(\langle X\rangle,\langle Y\rangle)$. The right panel plots coherence -$C=2|\rho_{01}|$, which is the distance from the origin in that plane for a -physical qubit state. All points describe the same final time; the colored curve -is a control-angle sweep, not a trajectory through time. - -```{code-cell} python -:tags: [hide-input] -import matplotlib.pyplot as plt -from matplotlib_inline.backend_inline import set_matplotlib_formats - -set_matplotlib_formats("svg") -plt.rcParams.update({ - "font.family": "serif", "font.serif": ["STIXGeneral"], "mathtext.fontset": "stix", - "font.size": 10, "axes.labelsize": 11, "axes.linewidth": 0.7, - "xtick.direction": "in", "ytick.direction": "in", - "xtick.top": True, "ytick.right": True, - "legend.frameon": False, "figure.constrained_layout.use": True, - "savefig.bbox": "tight", "svg.fonttype": "none", -}) -from matplotlib.collections import LineCollection -from matplotlib.patches import Circle - -predicted_states = predictions[1.0] -predicted_coherence = 2 * np.abs(predicted_states[:, 0, 1]) -no_pulse_coherence = predicted_coherence[0] -fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.4), gridspec_kw={"width_ratios": [1, 1.4]}) - -# Show the final states projected onto the equatorial Bloch plane. -bloch_xy = np.column_stack(( - 2 * predicted_states[:, 0, 1].real, - -2 * predicted_states[:, 0, 1].imag, -)) -points = bloch_xy[:, None, :] -segments = np.concatenate((points[:-1], points[1:]), axis=1) -trajectory = LineCollection(segments, cmap="twilight_shifted", - norm=plt.Normalize(0, 2 * np.pi), linewidth=2.6) -trajectory.set_array((pulse_angles[:-1] + pulse_angles[1:]) / 2) -axes[0].add_patch(Circle((0, 0), 1, facecolor="0.97", edgecolor="0.75", linewidth=0.8)) -axes[0].add_patch(Circle((0, 0), 0.5, fill=False, edgecolor="0.85", linewidth=0.6)) -axes[0].axhline(0, color="0.85", linewidth=0.6) -axes[0].axvline(0, color="0.85", linewidth=0.6) -axes[0].add_collection(trajectory) -axes[0].plot(*bloch_xy[0], "o", color="0.3", markerfacecolor="white", markersize=6) -axes[0].set(xlabel=r"$\langle X\rangle$", ylabel=r"$\langle Y\rangle$", - xlim=(-1.05, 1.05), ylim=(-1.05, 1.05), aspect="equal", - xticks=[-1, 0, 1], yticks=[-1, 0, 1]) -axes[0].set_title("(a) Final probe state", loc="left", fontsize=11) -colorbar = fig.colorbar(trajectory, ax=axes[0], orientation="horizontal", - shrink=0.8, pad=0.08, aspect=25, ticks=[0, np.pi, 2 * np.pi]) -colorbar.ax.set_xticklabels(["0", r"$\pi$", r"$2\pi$"]) -colorbar.set_label(r"Pulse angle $\theta$") - -axes[1].fill_between(pulse_angles, no_pulse_coherence, predicted_coherence, - where=predicted_coherence >= no_pulse_coherence, - interpolate=True, color="#0072B2", alpha=0.15) -axes[1].fill_between(pulse_angles, no_pulse_coherence, predicted_coherence, - where=predicted_coherence < no_pulse_coherence, - interpolate=True, color="#D55E00", alpha=0.2) -axes[1].plot(pulse_angles, predicted_coherence, color="#0072B2", linewidth=2.2, - label="With control pulse") -axes[1].axhline(no_pulse_coherence, color="0.4", linestyle="--", linewidth=1.1, - label="Free evolution") -axes[1].set(xlabel=r"Pulse angle $\theta$", ylabel=r"Final coherence $2|\rho_{01}|$", - xlim=(0, 2 * np.pi), ylim=(0, 1), - xticks=[0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi], - xticklabels=["0", r"$\pi/2$", r"$\pi$", r"$3\pi/2$", r"$2\pi$"]) -axes[1].set_title("(b) Predicted coherence", loc="left", fontsize=11) -axes[1].legend(loc="upper right", fontsize=9) -plt.show() +predicted = np.stack([ + characterizer.predict(model, rho0, sequence) for sequence in sequences +]) ``` -**A control pulse changes the final coherence.** The open circle marks free -evolution. Shading shows changes relative to that prediction. An instantaneous -$Z$ rotation preserves coherence magnitude at the moment it is applied; the -differences here arise during the subsequent joint evolution. A prediction alone -does not show whether the model has learned that response accurately, so we next -compare it with a reference. +`predict` returns a complex `(2, 2)` density-matrix estimate. Stacking the sweep +produces shape `(41, 2, 2)`. All entries describe the same final time, not a +trajectory through time. (short-horizon-validation)= -## 4. Check the predictions against joint evolution +## 3. Check against exact evolution -For two qubits, we can build the Hamiltonian directly with NumPy and propagate -with SciPy's matrix exponential. This reference uses neither the surrogate nor -YAQS's evolution routines. With site 0 as the least significant bit, the joint -initial vector is $|0\rangle_{\mathrm{env}}\otimes|+\rangle_{\mathrm{probe}}$, -and a probe rotation acts as $I\otimes R_z(\theta)$. +For two qubits, SciPy's matrix exponential gives a cheap reference independent +of YAQS's solvers. Site 0 is the least significant bit, so the joint initial +state is $|0\rangle_{\mathrm{env}}\otimes|+\rangle_{\mathrm{probe}}$ and a probe +pulse acts as $I\otimes R_z(\theta)$. Tracing out the environment gives the +reference probe state. ```{code-cell} python from scipy.linalg import expm pauli_x = np.array([[0, 1], [1, 0]], dtype=complex) pauli_z = np.diag([1.0, -1.0]) +dense_hamiltonian = -np.kron(pauli_z, pauli_z) - 0.5 * ( + np.kron(identity, pauli_x) + np.kron(pauli_x, identity) +) +evolution = expm(-1j * interval * dense_hamiltonian) initial_joint = np.kron([1, 0], plus) -references = {} -for coupling in couplings: - dense_hamiltonian = -coupling * np.kron(pauli_z, pauli_z) - field * ( - np.kron(identity, pauli_x) + np.kron(pauli_x, identity) - ) - evolution = expm(-1j * interval * dense_hamiltonian) - states = [] - for sequence in sequences: - pulse = sequence[1]["unitary"] - joint = evolution @ np.kron(identity, pulse) @ evolution @ initial_joint - amplitudes = joint.reshape(2, 2) - states.append(amplitudes.T @ amplitudes.conj()) - references[coupling] = np.stack(states) -``` - -Tracing out the environment gives the reference probe density matrix. Compare -the full matrix as well as the plotted coherence. We use half the trace norm of -the matrix difference, which equals trace distance when both matrices are -normalized physical states. - -```{code-cell} python -matrix_errors = {} -for coupling in couplings: - predicted = predictions[coupling] - reference = references[coupling] - matrix_errors[coupling] = 0.5 * np.sum(np.abs(np.linalg.eigvalsh(predicted - reference)), axis=1) - trace_error = np.max(np.abs(np.trace(predicted, axis1=1, axis2=2) - 1)) - hermiticity_error = np.max(np.abs(predicted - predicted.conj().swapaxes(1, 2))) - minimum_eigenvalue = np.min(np.linalg.eigvalsh(predicted)) - coherence_rmse = np.sqrt(np.mean((2 * np.abs(predicted[:, 0, 1]) - 2 * np.abs(reference[:, 0, 1])) ** 2)) - print( - f"J={coupling:g}: max matrix error={matrix_errors[coupling].max():.4f}, " - f"coherence RMSE={coherence_rmse:.4f}\n" - f" max trace error={trace_error:.4f}, " - f"Hermiticity error={hermiticity_error:.1e}, min eigenvalue={minimum_eigenvalue:.4f}" - ) +states = [] +for sequence in sequences: + joint = evolution @ np.kron(identity, sequence[1]["unitary"]) @ evolution @ initial_joint + amplitudes = joint.reshape(2, 2) + states.append(amplitudes.T @ amplitudes.conj()) +reference = np.stack(states) + +matrix_errors = 0.5 * np.sum(np.abs(np.linalg.eigvalsh(predicted - reference)), axis=1) +trace_error = np.max(np.abs(np.trace(predicted, axis1=1, axis2=2) - 1)) +minimum_eigenvalue = np.min(np.linalg.eigvalsh(predicted)) +print(f"Maximum matrix error: {matrix_errors.max():.4f}") +print(f"Maximum trace error: {trace_error:.4f}; minimum eigenvalue: {minimum_eigenvalue:.4f}") ``` -The public API makes each returned estimate Hermitian, so a zero Hermiticity -error is expected. It does not enforce unit trace or positivity. The printed -checks expose normalization error and any negative eigenvalues; we do not -renormalize the predictions, clip their eigenvalues, or project them onto -physical states. A positive minimum eigenvalue alone is insufficient when the -trace differs from one. - -## 5. Compare environmental couplings - -The second model asks whether the same control has a different effect when the -probe couples more weakly to its environment. Plot both coherence sweeps with -their independent references, then show where the full predicted matrices differ -from those references. +The matrix error is half the trace norm of the difference. It equals trace +distance when both matrices are normalized physical states. The public API makes +predictions Hermitian but does not enforce unit trace or positivity. The printed +checks expose those limitations; we do not normalize the predictions or clip +negative eigenvalues. ```{code-cell} python :tags: [hide-input] -fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.1)) -colors = ["#D55E00", "#0072B2"] -for coupling, color in zip(couplings, colors, strict=True): - coherence = 2 * np.abs(predictions[coupling][:, 0, 1]) - exact_coherence = 2 * np.abs(references[coupling][:, 0, 1]) - axes[0].plot(pulse_angles, coherence, color=color, linewidth=2, label=rf"Surrogate, $J={coupling:g}$") - axes[0].plot(pulse_angles[::5], exact_coherence[::5], "o", color=color, - markerfacecolor="white", markersize=4, markeredgewidth=1) - axes[1].plot(pulse_angles, matrix_errors[coupling], color=color, linewidth=2, label=rf"$J={coupling:g}$") -axes[0].plot([], [], "o", color="0.3", markerfacecolor="white", markersize=4, label="Joint evolution") -axes[0].set(ylabel=r"Final coherence $2|\rho_{01}|$") -axes[1].set(ylabel=r"Matrix error $\frac{1}{2}\|\rho_{\rm pred}-\rho_{\rm ref}\|_1$") +import matplotlib.pyplot as plt +from matplotlib_inline.backend_inline import set_matplotlib_formats + +set_matplotlib_formats("svg") +plt.rcParams.update({ + "font.family": "serif", "font.serif": ["STIXGeneral"], "mathtext.fontset": "stix", + "font.size": 10, "axes.labelsize": 11, "axes.linewidth": 0.7, + "xtick.direction": "in", "ytick.direction": "in", + "xtick.top": True, "ytick.right": True, + "legend.frameon": False, "figure.constrained_layout.use": True, + "savefig.bbox": "tight", "svg.fonttype": "none", +}) +fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.8)) +axes[0].plot(pulse_angles, 2 * np.abs(predicted[:, 0, 1]), color="#0072B2", + linewidth=2, label="Surrogate") +axes[0].plot(pulse_angles[::2], 2 * np.abs(reference[::2, 0, 1]), "o", color="#D55E00", + markerfacecolor="white", markersize=4, label="Exact evolution") +axes[0].set_ylabel(r"Final coherence $2|\rho_{01}|$") +axes[0].set_title("(a) Response to a control pulse", loc="left", fontsize=10) +axes[0].legend(fontsize=9) +axes[1].plot(pulse_angles, matrix_errors, color="#0072B2", linewidth=2) +axes[1].fill_between(pulse_angles, 0, matrix_errors, color="#0072B2", alpha=0.12) +axes[1].set_ylabel(r"Matrix error $\frac{1}{2}\|\rho_{\rm pred}-\rho_{\rm ref}\|_1$") +axes[1].set_title("(b) Error against exact evolution", loc="left", fontsize=10) +axes[1].set_ylim(bottom=0) for ax in axes: ax.set(xlabel=r"Pulse angle $\theta$", xlim=(0, 2 * np.pi), xticks=[0, np.pi, 2 * np.pi], xticklabels=["0", r"$\pi$", r"$2\pi$"]) - ax.legend(fontsize=8, loc="best") -axes[1].set_ylim(bottom=0) -axes[0].set_title("(a) Coupling changes the control response", loc="left", fontsize=10) -axes[1].set_title("(b) Error against joint evolution", loc="left", fontsize=10) plt.show() ``` -**The same pulse produces different responses at the two couplings.** Solid -curves show surrogate predictions; open circles show direct evolution. The error -panel and printed state checks bound what we can infer from those curves. -Accuracy on random validation sequences does not certify a chosen pulse family, -and this two-step comparison says nothing about longer sequences. Repeat the -reference checks when changing the Hamiltonian, environment preparation, control -family, or time horizon. - -## Other supported options - -`predict(model, rho0, sequence, return_sequence=True)` returns an array of shape -`(num_interventions, 2, 2)`. Its entries describe the probe after each -intervention and its following evolution interval, not a continuous time trace. -Predictions for earlier steps still use the trained schedule. - -For other control families, `intervention_style` also accepts `"clifford"` and -`"measure_prepare"` when sampling or training. Prediction sequences accept -unitary dictionaries as above, or style strings that draw random controls. A -`"measure_prepare"` draw selects a rank-one measurement outcome and a -replacement state. Match the training controls to the intended queries and -validate other interventions separately; this example tests only unitary -controls. See {doc}`characterization` for memory diagnostics. - -Short process tensors provide another reference through `build_process_tensor` -and the same `predict` call. They start from the joint all-zero state, and their -input must match `process_tensor.initial_rho`. They therefore do not directly -match the $|+\rangle$ preparation used here. The default uncapped MPO -construction grows as `16**num_interventions`; dense tomography also grows -exponentially. Use these references for short horizons and see -{doc}`characterization` for approximation limits, QMI, and CMI. Those -process-tensor diagnostics are distinct from prediction accuracy and from the -response-matrix memory spectrum. +An instantaneous $Z$ rotation preserves coherence magnitude when applied. The +variation here arises during subsequent interaction with the environment. The +reference and error panel show how well this small model captures that response. +This training budget keeps the example inexpensive and leaves visible prediction +errors. Results can vary with seeds and PyTorch versions. + +## Scope and other options + +The model uses a fixed Hamiltonian, environment preparation, and control +schedule. Retrain and validate when those change. The public training path does +not accept a `NoiseModel`, and this two-intervention example does not establish +accuracy for longer protocols. + +Use `predict(model, rho0, sequence, return_sequence=True)` to obtain shape +`(num_interventions, 2, 2)`, with one state after each intervention and its +following evolution interval. Sampling and training also support `"clifford"` +and `"measure_prepare"` controls; validate other control families separately. +For environmental memory diagnostics and short process-tensor references, see +{doc}`characterization`. diff --git a/docs/examples/quickstart.md b/docs/examples/quickstart.md index 32e0353ba..6a0217c1f 100644 --- a/docs/examples/quickstart.md +++ b/docs/examples/quickstart.md @@ -437,127 +437,11 @@ and validation. ## Predict non-Markovian dynamics -Train a surrogate on **random unitary controls**, then explore how a pulse -changes the coherence of a probe coupled to an environment spin. Install the -`torch` extra first: `uv pip install "mqt.yaqs[torch]"`. - -```{note} -**Experimental feature.** Surrogate modeling is not yet supported by a published -YAQS paper. This example uses a short, two-intervention horizon. Validate -predictions for your chosen controls and time horizon against reference -simulations or measurements. -``` - -```{code-cell} python -import numpy as np -import torch - -from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer - -num_steps = 2 -interval = 0.6 -hamiltonian = Hamiltonian.ising(2, J=1.0, g=0.5) -params = AnalogSimParams(elapsed_time=interval, dt=interval, preset="fast") -characterizer = MemoryCharacterizer(show_progress=False) -``` - -Train on 4,096 random sequences and select the model using 256 fresh random -validation sequences. The environment starts in $|0\rangle$. The folded cell -sets a small model and training budget for this documentation example. - -```{code-cell} python -:tags: [hide-input] -torch.manual_seed(7) -schedule = [0.0] + [interval] * num_steps -validation = characterizer.sample( - hamiltonian, params, num_interventions=num_steps, n=256, seed=99, - timesteps=schedule, intervention_style="haar", -) -model = characterizer.train( - hamiltonian, params, num_interventions=num_steps, n=4096, seed=7, - timesteps=schedule, intervention_style="haar", - model_kwargs={"d_model": 64, "num_layers": 2, "dim_ff": 128}, - train_kwargs={"epochs": 400, "lr": 1e-3, "val_dataset": validation}, -) -``` - -Start the probe in $|+\rangle$. Let it evolve for $t=0.6$, apply a rotation -$R_z(\theta)$, then predict its state at $t=1.2$ for each pulse angle. These -chosen sequences are not supplied during training. - -```{code-cell} python -plus = np.array([1, 1], dtype=complex) / np.sqrt(2) -rho0 = np.outer(plus, plus.conj()) -identity = np.eye(2, dtype=complex) -pulse_angles = np.linspace(0, 2 * np.pi, 61) -predicted_states = np.asarray([ - characterizer.predict(model, rho0, [ - {"unitary": identity}, - {"unitary": np.diag(np.exp(-0.5j * angle * np.array([1, -1])))}, - ]) - for angle in pulse_angles -]) -``` - -```{code-cell} python -:tags: [hide-input] -from matplotlib.collections import LineCollection -from matplotlib.patches import Circle - -predicted_coherence = 2 * np.abs(predicted_states[:, 0, 1]) -no_pulse_coherence = predicted_coherence[0] -fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.4), gridspec_kw={"width_ratios": [1, 1.4]}) - -# Show the final states projected onto the equatorial Bloch plane. -bloch_xy = np.column_stack(( - 2 * predicted_states[:, 0, 1].real, - -2 * predicted_states[:, 0, 1].imag, -)) -points = bloch_xy[:, None, :] -segments = np.concatenate((points[:-1], points[1:]), axis=1) -trajectory = LineCollection(segments, cmap="twilight_shifted", - norm=plt.Normalize(0, 2 * np.pi), linewidth=2.6) -trajectory.set_array((pulse_angles[:-1] + pulse_angles[1:]) / 2) -axes[0].add_patch(Circle((0, 0), 1, facecolor="0.97", edgecolor="0.75", linewidth=0.8)) -axes[0].add_patch(Circle((0, 0), 0.5, fill=False, edgecolor="0.85", linewidth=0.6)) -axes[0].axhline(0, color="0.85", linewidth=0.6) -axes[0].axvline(0, color="0.85", linewidth=0.6) -axes[0].add_collection(trajectory) -axes[0].plot(*bloch_xy[0], "o", color="0.3", markerfacecolor="white", markersize=6) -axes[0].set(xlabel=r"$\langle X\rangle$", ylabel=r"$\langle Y\rangle$", - xlim=(-1.05, 1.05), ylim=(-1.05, 1.05), aspect="equal", - xticks=[-1, 0, 1], yticks=[-1, 0, 1]) -axes[0].set_title("(a) Final probe state", loc="left", fontsize=11) -colorbar = fig.colorbar(trajectory, ax=axes[0], orientation="horizontal", - shrink=0.8, pad=0.08, aspect=25, ticks=[0, np.pi, 2 * np.pi]) -colorbar.ax.set_xticklabels(["0", r"$\pi$", r"$2\pi$"]) -colorbar.set_label(r"Pulse angle $\theta$") - -axes[1].fill_between(pulse_angles, no_pulse_coherence, predicted_coherence, - where=predicted_coherence >= no_pulse_coherence, - interpolate=True, color="#0072B2", alpha=0.15) -axes[1].fill_between(pulse_angles, no_pulse_coherence, predicted_coherence, - where=predicted_coherence < no_pulse_coherence, - interpolate=True, color="#D55E00", alpha=0.2) -axes[1].plot(pulse_angles, predicted_coherence, color="#0072B2", linewidth=2.2, - label="With control pulse") -axes[1].axhline(no_pulse_coherence, color="0.4", linestyle="--", linewidth=1.1, - label="Free evolution") -axes[1].set(xlabel=r"Pulse angle $\theta$", ylabel=r"Final coherence $2|\rho_{01}|$", - xlim=(0, 2 * np.pi), ylim=(0, 1), - xticks=[0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi], - xticklabels=["0", r"$\pi/2$", r"$\pi$", r"$3\pi/2$", r"$2\pi$"]) -axes[1].set_title("(b) Predicted coherence", loc="left", fontsize=11) -axes[1].legend(loc="upper right", fontsize=9) -plt.show() -``` - -The left panel shows the predicted final probe states in the Bloch plane; color -identifies the pulse angle, and the open circle marks free evolution. The right -panel shows their coherence. Blue shading marks an increase over free evolution; -orange marks a decrease. All points use the same trained model and include both -evolution intervals. See {doc}`memory_surrogate` for validation and checks of -predicted density matrices. +YAQS also provides experimental surrogate models that learn a probe's response +to controls from simulated training sequences. See {doc}`memory_surrogate` for a +small example with an independent reference. This feature is not yet supported +by a published YAQS paper; validate predictions for your controls and time +horizon. ## Next steps diff --git a/docs/index.md b/docs/index.md index 022d68ac0..d3aa9dffe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -104,7 +104,7 @@ Circuit verification :titlesonly: Ensemble evolution -Non-Markovian transformer models (experimental) +Non-Markovian surrogate models (experimental) ``` From d3f16427fe669ab2669ca1182dc09e4e3007db9e Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:58:17 +0200 Subject: [PATCH 29/30] reduced trajectory budget --- docs/examples/transmon_emulation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/transmon_emulation.md b/docs/examples/transmon_emulation.md index cd04b5990..dcf762606 100644 --- a/docs/examples/transmon_emulation.md +++ b/docs/examples/transmon_emulation.md @@ -104,7 +104,7 @@ params = AnalogSimParams( elapsed_time=transfer_time, dt=transfer_time / 80, order=2, - num_traj=32, + num_traj=24, preset="balanced", random_seed=7, ) @@ -117,7 +117,7 @@ state; local matrix observables also work with higher levels. The grid contains 81 samples over one transfer interval. The `balanced` preset sets numerical tolerances; `order=2` selects second-order TJM for noisy runs. -Each noisy calculation averages 32 trajectories. Sampling error, timestep error, +Each noisy calculation averages 24 trajectories. Sampling error, timestep error, and the chosen level cutoffs need separate convergence checks. ## 4. Follow the noiseless transfer From f4a95aa1e1bd87f8d9a9036dbac61c340574c788 Mon Sep 17 00:00:00 2001 From: Aaron Sander <61705296+aaronleesander@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:50:23 +0200 Subject: [PATCH 30/30] docs plots now executed as part of CI --- .github/workflows/ci.yml | 25 + .pre-commit-config.yaml | 4 + .readthedocs.yaml | 7 +- docs/_ext/yaqs_examples.py | 189 + docs/_outputs/README.md | 33 + .../_outputs/examples/analog_simulation.ipynb | 3873 +++++ docs/_outputs/examples/characterization.ipynb | 2544 ++++ .../examples/circuit_observables.ipynb | 5196 +++++++ docs/_outputs/examples/circuit_shots.ipynb | 3564 +++++ .../examples/digital_analog_simulation.ipynb | 11741 +++++++++++++++ docs/_outputs/examples/digital_twin.ipynb | 2206 +++ .../examples/ensemble_evolution.ipynb | 1737 +++ .../examples/equivalence_checking.ipynb | 1233 ++ docs/_outputs/examples/hamiltonians.ipynb | 443 + docs/_outputs/examples/memory_surrogate.ipynb | 1020 ++ docs/_outputs/examples/quickstart.ipynb | 12241 ++++++++++++++++ .../examples/realistic_noise_models.ipynb | 525 + .../examples/representation_comparison.ipynb | 3992 +++++ .../examples/simulation_parameters.ipynb | 357 + .../examples/simulator_initialization.ipynb | 301 + .../examples/state_initialization.ipynb | 368 + .../examples/transmon_emulation.ipynb | 4280 ++++++ docs/_outputs/examples/trapped_ion.ipynb | 4058 +++++ docs/_outputs/manifest.json | 85 + docs/conf.py | 4 +- noxfile.py | 34 +- pyproject.toml | 2 + tests/docs/test_build.py | 69 +- uv.lock | 16 +- 29 files changed, 60129 insertions(+), 18 deletions(-) create mode 100644 docs/_ext/yaqs_examples.py create mode 100644 docs/_outputs/README.md create mode 100644 docs/_outputs/examples/analog_simulation.ipynb create mode 100644 docs/_outputs/examples/characterization.ipynb create mode 100644 docs/_outputs/examples/circuit_observables.ipynb create mode 100644 docs/_outputs/examples/circuit_shots.ipynb create mode 100644 docs/_outputs/examples/digital_analog_simulation.ipynb create mode 100644 docs/_outputs/examples/digital_twin.ipynb create mode 100644 docs/_outputs/examples/ensemble_evolution.ipynb create mode 100644 docs/_outputs/examples/equivalence_checking.ipynb create mode 100644 docs/_outputs/examples/hamiltonians.ipynb create mode 100644 docs/_outputs/examples/memory_surrogate.ipynb create mode 100644 docs/_outputs/examples/quickstart.ipynb create mode 100644 docs/_outputs/examples/realistic_noise_models.ipynb create mode 100644 docs/_outputs/examples/representation_comparison.ipynb create mode 100644 docs/_outputs/examples/simulation_parameters.ipynb create mode 100644 docs/_outputs/examples/simulator_initialization.ipynb create mode 100644 docs/_outputs/examples/state_initialization.ipynb create mode 100644 docs/_outputs/examples/transmon_emulation.ipynb create mode 100644 docs/_outputs/examples/trapped_ion.ipynb create mode 100644 docs/_outputs/manifest.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a379b1482..93b0708ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,30 @@ jobs: - name: Check documentation run: uvx nox --non-interactive -s docs-check + docs-execute: + name: 📊 Documentation examples + runs-on: ubuntu-24.04 + timeout-minutes: 60 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + - name: Execute documentation examples + run: uvx nox --non-interactive -s docs-execute + - name: Save executed examples and rendered documentation + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: documentation-examples + path: | + docs/_outputs + docs/_build/executed + if-no-files-found: error + retention-days: 14 + build-sdist: name: 🚀 CD needs: change-detection @@ -105,6 +129,7 @@ jobs: - python-coverage - python-linter - docs-check + - docs-execute - build-sdist - build-wheel runs-on: ubuntu-slim diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82f4ec78f..e0c01e93d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,10 @@ # prek install # +# Saved notebooks are generated from the checked Markdown cells. Formatting their +# copies would invalidate the output manifest and repeat checks of the same code. +exclude: ^docs/_outputs/.*\.ipynb$ + ci: autoupdate_commit_msg: "⬆️🪝 update pre-commit hooks" autoupdate_schedule: quarterly diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 95b988988..366ba69d9 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,10 +10,10 @@ build: python: "3.14" jobs: install: - - uv pip install --python "$READTHEDOCS_VIRTUALENV_PATH/bin/python" --group docs --torch-backend cpu --exact -e '.[qasm3,torch]' + - uv pip install --python "$READTHEDOCS_VIRTUALENV_PATH/bin/python" --group docs --exact -e . build: html: - # Execute one notebook at a time; each simulation has its own worker budget. + # Render saved notebook outputs; execute and validate examples in GitHub Actions. - >- "$READTHEDOCS_VIRTUALENV_PATH/bin/python" -u -m sphinx -j 1 -n -T -W --keep-going -b html -d docs/_build/doctrees @@ -24,6 +24,3 @@ python: - method: uv command: pip path: . - extras: - - qasm3 - - torch diff --git a/docs/_ext/yaqs_examples.py b/docs/_ext/yaqs_examples.py new file mode 100644 index 000000000..88933ebd9 --- /dev/null +++ b/docs/_ext/yaqs_examples.py @@ -0,0 +1,189 @@ +# Copyright (c) 2025 - 2026 Chair for Design Automation, TUM +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Publish saved notebook outputs and regenerate them in a separate build.""" + +from __future__ import annotations + +import hashlib +import json +import os +import platform +from importlib.metadata import version +from pathlib import Path +from typing import TYPE_CHECKING + +import nbformat +from jupyter_cache import get_cache +from jupyter_cache.base import CacheBundleIn +from myst_nb.core.read import is_myst_markdown_notebook, read_myst_markdown_notebook +from sphinx.errors import SphinxError + +if TYPE_CHECKING: + from nbformat import NotebookNode + from sphinx.application import Sphinx + + +def _notebooks(app: Sphinx) -> dict[Path, NotebookNode]: + """Read notebook sources, excluding generated documentation. + + Returns: + Source paths relative to the documentation directory and their notebooks. + """ + source = Path(app.srcdir) + notebooks = {} + for path in sorted(source.rglob("*")): + relative = path.relative_to(source) + if relative.parts[0] in {"_build", "_outputs"}: + continue + if path.suffix == ".ipynb": + notebooks[relative] = nbformat.read(path, as_version=4) + elif path.suffix == ".md": + text = path.read_text(encoding="utf-8") + if is_myst_markdown_notebook(text): + notebooks[relative] = read_myst_markdown_notebook( + text, config=app.env.myst_config, add_source_map=True, path=path + ) + return notebooks + + +def _runtime_digest(root: Path) -> str: + """Hash package code and dependency inputs that can change example results. + + Returns: + A digest independent of paths, timestamps, and generated version strings. + """ + digest = hashlib.sha256() + paths = [*sorted((root / "src").rglob("*.py")), root / "pyproject.toml", root / "uv.lock"] + for path in paths: + if path.is_file() and path.name != "_version.py": + digest.update(path.relative_to(root).as_posix().encode()) + digest.update(b"\0") + digest.update(path.read_bytes()) + digest.update(b"\0") + return digest.hexdigest() + + +def _input_digest(notebook: NotebookNode) -> str: + """Hash executable cells and settings, allowing prose-only edits. + + Returns: + The digest of inputs used to produce notebook outputs. + """ + cells = [ + {"source": cell.source, "metadata": {key: value for key, value in cell.metadata.items() if key != "source_map"}} + for cell in notebook.cells + if cell.cell_type == "code" + ] + metadata = {key: value for key, value in notebook.metadata.items() if key != "source_map"} + payload = json.dumps({"metadata": metadata, "cells": cells}, sort_keys=True).encode() + return hashlib.sha256(payload).hexdigest() + + +def _prepare_outputs(app: Sphinx) -> None: + """Require current saved outputs before publishing, then seed MyST's cache. + + Raises: + SphinxError: Saved outputs are missing, stale, or cannot be used safely. + """ + notebooks = _notebooks(app) + if app.env.mystnb_config.execution_mode != "cache" or any( + notebook.metadata.get("mystnb", {}).get("execution_mode", "cache") != "cache" for notebook in notebooks.values() + ): + msg = "Documentation requires nb_execution_mode='cache' to publish saved outputs safely." + raise SphinxError(msg) + if app.config.yaqs_generate_examples: + return + output = Path(app.srcdir) / "_outputs" + instruction = "Regenerate example outputs with: uvx nox --non-interactive -s docs-execute" + try: + manifest = json.loads((output / "manifest.json").read_text(encoding="utf-8")) + except (OSError, ValueError) as error: + msg = f"Missing or invalid example output manifest. {instruction}" + raise SphinxError(msg) from error + if manifest.get("runtime") != _runtime_digest(Path(app.srcdir).parent): + msg = f"Example outputs predate changes to package code or dependencies. {instruction}" + raise SphinxError(msg) + cache = get_cache(app.env.mystnb_config.execution_cache_path) + for relative, notebook in notebooks.items(): + entry = manifest.get("notebooks", {}).get(relative.as_posix()) + saved = output / relative.with_suffix(".ipynb") + if entry is None or entry["inputs"] != _input_digest(notebook): + msg = f"Missing or stale example outputs for {relative}. {instruction}" + raise SphinxError(msg) + if not saved.is_file() or hashlib.sha256(saved.read_bytes()).hexdigest() != entry["sha256"]: + msg = f"Missing or changed saved notebook {saved}. {instruction}" + raise SphinxError(msg) + executed = nbformat.read(saved, as_version=4) + cache.cache_notebook_bundle(CacheBundleIn(executed, str(Path(app.srcdir) / relative)), overwrite=True) + try: + cache.match_cache_notebook(notebook) + except KeyError as error: + msg = f"Saved outputs do not match executable cells in {relative}. {instruction}" + raise SphinxError(msg) from error + + +def _output_dependencies(app: Sphinx, docname: str, source: list[str]) -> None: + """Refresh rendered pages when their saved outputs change.""" + del source + output = Path(app.srcdir) / "_outputs" / f"{docname}.ipynb" + if output.is_file(): + app.env.note_dependency(str(output)) + app.env.note_dependency(str(Path(app.srcdir) / "_outputs" / "manifest.json")) + + +def _save_outputs(app: Sphinx, exception: Exception | None) -> None: + """Export successfully executed notebooks and record their input provenance.""" + if ( + exception is not None + or app.statuscode + or not app.config.yaqs_generate_examples + or app.tags.has("sphinx_llm_markdown") + ): + return + output = Path(app.srcdir) / "_outputs" + output.mkdir(parents=True, exist_ok=True) + executed_dir = Path(app.env.mystnb_config.output_folder) + entries = {} + for relative, notebook in _notebooks(app).items(): + executed = nbformat.read(executed_dir / relative.with_suffix(".ipynb"), as_version=4) + for index, cell in enumerate(executed.cells): + cell.id = f"cell-{index}" + cell.metadata.pop("execution", None) + saved = output / relative.with_suffix(".ipynb") + saved.parent.mkdir(parents=True, exist_ok=True) + nbformat.write(executed, saved) + entries[relative.as_posix()] = { + "inputs": _input_digest(notebook), + "sha256": hashlib.sha256(saved.read_bytes()).hexdigest(), + } + retained = {output / Path(name).with_suffix(".ipynb") for name in entries} + for saved in output.rglob("*.ipynb"): + if saved not in retained: + saved.unlink() + manifest = { + "runtime": _runtime_digest(Path(app.srcdir).parent), + "environment": { + "python": platform.python_version(), + **{name: version(name) for name in ("mqt.yaqs", "numpy", "scipy", "qiskit", "myst-nb")}, + }, + "notebooks": entries, + } + (output / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + + +def setup(app: Sphinx) -> dict[str, bool]: + """Register saved-output publishing and generation hooks. + + Returns: + Parallel build support declarations. + """ + app.add_config_value("yaqs_generate_examples", os.environ.get("YAQS_DOCS_EXECUTE") == "1", "env") + app.connect("builder-inited", _prepare_outputs, priority=600) + app.connect("source-read", _output_dependencies) + app.connect("build-finished", _save_outputs, priority=400) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/_outputs/README.md b/docs/_outputs/README.md new file mode 100644 index 000000000..b3956d0a9 --- /dev/null +++ b/docs/_outputs/README.md @@ -0,0 +1,33 @@ +# Saved example outputs + +The Markdown files in `docs/examples/` remain the guide sources. This directory +stores their executed notebooks so Read the Docs can publish code, figures, and +results without running simulations or installing PyTorch. + +After changing example code, package code, or dependencies, regenerate the +outputs: + +```console +uvx nox --non-interactive -s docs-execute +``` + +Review the rendered pages in `docs/_build/executed/` and commit the notebooks +and `manifest.json` with the source changes. Edit the Markdown sources rather +than the saved notebooks. Changes to prose do not require another execution. + +The usual documentation command renders the saved outputs: + +```console +uvx nox --non-interactive -s docs +``` + +Publishing fails if outputs are missing, changed, or stale. The manifest records +the executable cells, notebook settings, package source, dependency inputs, and +execution environment. Generated version strings do not invalidate outputs. +Examples that read additional data files must also include those files in the +input check in `docs/_ext/yaqs_examples.py`. + +GitHub Actions executes all examples in a separate job and uploads the notebooks +and rendered pages as the `documentation-examples` artifact. The job does not +commit changes. Download and review the artifact if you regenerate outputs in +CI. The documentation check validates the committed copies independently. diff --git a/docs/_outputs/examples/analog_simulation.ipynb b/docs/_outputs/examples/analog_simulation.ipynb new file mode 100644 index 000000000..6871fd059 --- /dev/null +++ b/docs/_outputs/examples/analog_simulation.ipynb @@ -0,0 +1,3873 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Noisy Analog Simulation\n", + "\n", + "Relaxation can remove an excitation before it travels across a spin chain. To\n", + "study this competition between transport and loss, we follow an excitation\n", + "through a **20-site XY chain** and compare its motion at several relaxation\n", + "rates. The occupation heatmaps show where the excitation travels, while the\n", + "total occupation tells us how much survives.\n", + "\n", + "This guide extends the analog example in {doc}`quickstart` using only the\n", + "standard YAQS installation. YAQS represents the state as a matrix product state\n", + "(MPS) and simulates noise with the tensor jump method (TJM), averaging\n", + "independent quantum-jump trajectories. Run the cells in order in a notebook. In\n", + "a script, put execution inside an `if __name__ == \"__main__\":` guard, as shown\n", + "in {doc}`simulator_initialization`.\n", + "\n", + "## 1. Build the Hamiltonian\n", + "\n", + "The XY model lets an excitation move between neighboring spins without changing\n", + "the total number of excitations. With open ends and the coefficients below, its\n", + "Hamiltonian is\n", + "\n", + "$$\n", + "H=-\\frac{1}{2}\\sum_{i=0}^{L-2}(X_iX_{i+1}+Y_iY_{i+1}).\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import Hamiltonian\n", + "\n", + "length = 20\n", + "hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "Setting `Jz=0` removes the ZZ interaction from the Heisenberg model. The default\n", + "boundary condition is open, so sites 0 and 19 have one neighbor each. Because\n", + "the Hamiltonian conserves excitation, a noiseless run gives us a baseline\n", + "against which to measure relaxation.\n", + "\n", + "We use $\\hbar=1$ and set the excitation-hopping amplitude to one. Time is\n", + "measured in its inverse units. See {doc}`hamiltonians` for other built-in\n", + "models, custom terms, and boundary conditions.\n", + "\n", + "## 2. Prepare one localized excitation\n", + "\n", + "To see the excitation spread, start every spin in $|0\\rangle$ except the spin at\n", + "site 10, which starts in $|1\\rangle$. Each character of `basis_string` specifies\n", + "the state of the corresponding site, starting at site 0." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import State\n", + "\n", + "center = length // 2\n", + "basis = \"0\" * center + \"1\" + \"0\" * (length - center - 1)\n", + "state = State(length, initial=\"basis\", basis_string=basis)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "`State` uses an MPS by default. A single excitation keeps entanglement modest,\n", + "so this example is inexpensive compared with a general interacting state on 20\n", + "sites. For other initial states and representations, see\n", + "{doc}`state_initialization` and {doc}`representation_comparison`.\n", + "\n", + "## 3. Choose observables, times, and accuracy\n", + "\n", + "The spatial dynamics require one observable per site. We use $Z_i$, whose\n", + "expectation gives the occupation through\n", + "$\\langle n_i\\rangle=(1-\\langle Z_i\\rangle)/2$." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import AnalogSimParams, Observable\n", + "\n", + "params = AnalogSimParams(\n", + " observables=[Observable(\"z\", site) for site in range(length)],\n", + " elapsed_time=3.0,\n", + " dt=0.25,\n", + " num_traj=32,\n", + " preset=\"fast\",\n", + " random_seed=7,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "These settings sample 13 times from $t=0$ to $t=3$, long enough to see the\n", + "excitation spread away from the center. Time sampling is enabled by default, and\n", + "`elapsed_time` must be an integer multiple of `dt`.\n", + "\n", + "The `fast` preset sets numerical tolerances for a quick example. We override its\n", + "trajectory budget with `num_traj=32`, so each noisy run averages 32\n", + "trajectories; the noiseless calculation needs only one. Increasing this budget\n", + "reduces sampling error, while timestep and MPS truncation errors require\n", + "separate convergence checks.\n", + "\n", + "`random_seed` fixes the jump random streams for repeat runs with the same\n", + "configuration. It does not improve accuracy. See {doc}`simulation_parameters`\n", + "for presets, overrides, and convergence settings.\n", + "\n", + "## 4. Define relaxation\n", + "\n", + "Relaxation competes with the motion generated by the Hamiltonian. The `lowering`\n", + "channel turns $|1\\rangle$ into $|0\\rangle$ without adding new excitations. Give\n", + "every site the same relaxation rate $\\gamma$:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import NoiseModel\n", + "\n", + "relaxation_rate = 4.0\n", + "noise = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": relaxation_rate}\n", + " for site in range(length)\n", + "])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "For analog evolution, `strength` is a Lindblad rate, with units of inverse time.\n", + "YAQS forms the jump operator $\\sqrt{\\gamma}\\,|0\\rangle\\langle1|$ internally;\n", + "supply $\\gamma$, not its square root.\n", + "\n", + "The corresponding lifetime is $1/\\gamma$. At $\\gamma=4$, loss occurs on a\n", + "shorter timescale than hopping between sites. For site-dependent rates, other\n", + "channels, custom operators, and distribution-valued strengths, see\n", + "{doc}`realistic_noise_models`.\n", + "\n", + "## 5. Run the noiseless and noisy cases\n", + "\n", + "Initialize the simulator separately, then pass the state, Hamiltonian, and\n", + "parameters to `run`. Omit the noise model for the noiseless baseline." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import Simulator\n", + "\n", + "simulator = Simulator(show_progress=False)\n", + "noiseless = simulator.run(state, hamiltonian, params)\n", + "noisy = simulator.run(state, hamiltonian, params, noise)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "Parallel execution remains enabled by default. `show_progress=False` keeps the\n", + "documentation quiet; omit it to see progress. Execution and worker options are\n", + "explained in {doc}`simulator_initialization`.\n", + "\n", + "## 6. Read the results\n", + "\n", + "`expectation_values` follows the order of the supplied observables. Here, row\n", + "$i$ contains $\\langle Z_i\\rangle$ and columns follow `times`. Convert the\n", + "expectations to an array of occupations:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(20, 13)\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "times = np.asarray(noisy.times)\n", + "occupation = (1 - np.asarray(noisy.expectation_values)) / 2\n", + "print(occupation.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "The shape is `(20, 13)`: sites by sampled times. At $t=0$, only site 10 has\n", + "occupation one. In noisy runs, each entry is a trajectory average, rather than a\n", + "single measurement outcome. `noisy.trajectories` retains the per-trajectory\n", + "observable data when you need to examine sampling fluctuations.\n", + "\n", + "## 7. Compare four relaxation strengths\n", + "\n", + "The relaxation lifetime determines how long the excitation can propagate before\n", + "loss. To compare that timescale with the transport dynamics, keep the model and\n", + "numerical settings fixed and add rates between the two runs above:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": {}, + "outputs": [], + "source": [ + "rates = [0.0, 0.5, 1.5, relaxation_rate]\n", + "results = {0.0: noiseless, relaxation_rate: noisy}\n", + "for rate in rates[1:-1]:\n", + " rate_noise = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": rate}\n", + " for site in range(length)\n", + " ])\n", + " results[rate] = simulator.run(state, hamiltonian, params, rate_noise)\n", + "\n", + "occupations = np.stack([\n", + " (1 - np.asarray(results[rate].expectation_values)) / 2\n", + " for rate in rates\n", + "])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "The nonzero rates give lifetimes of $2$, $2/3$, and $1/4$ in the time units used\n", + "here. The heatmaps below compare how far the excitation spreads before its\n", + "occupation decays. All panels use one square-root color scale to keep small\n", + "occupations visible, with no normalization by the remaining excitation." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cell-15", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:34:59.410121\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib.colors import PowerNorm\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIXGeneral\"],\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 11,\n", + " \"axes.labelsize\": 11,\n", + " \"axes.linewidth\": 0.7,\n", + " \"xtick.labelsize\": 10,\n", + " \"ytick.labelsize\": 10,\n", + " \"xtick.direction\": \"in\",\n", + " \"ytick.direction\": \"in\",\n", + " \"xtick.top\": True,\n", + " \"ytick.right\": True,\n", + " \"legend.fontsize\": 9,\n", + " \"legend.frameon\": False,\n", + " \"lines.linewidth\": 1.6,\n", + " \"figure.constrained_layout.use\": True,\n", + " \"savefig.dpi\": 180,\n", + "})\n", + "fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.4), sharex=True, sharey=True)\n", + "for ax, values, rate, panel in zip(\n", + " axes.flat, occupations, rates, \"abcd\", strict=True,\n", + "):\n", + " image = ax.pcolormesh(\n", + " times, np.arange(length), values, shading=\"auto\", cmap=\"cividis\",\n", + " norm=PowerNorm(0.5, vmin=0, vmax=1), rasterized=True,\n", + " )\n", + " label = \"Noiseless\" if rate == 0 else rf\"$\\gamma={rate:g}$\"\n", + " ax.set_title(f\"({panel}) {label}\", loc=\"left\", fontsize=11)\n", + " ax.set(xticks=[0, 1, 2, 3], yticks=[0, 5, 10, 15, 19])\n", + "for ax in axes[-1]:\n", + " ax.set_xlabel(r\"Time $t$\")\n", + "for ax in axes[:, 0]:\n", + " ax.set_ylabel(r\"Site $i$\")\n", + "fig.colorbar(image, ax=axes.ravel().tolist(),\n", + " label=r\"Occupation $\\langle n_i\\rangle$\", ticks=[0, 0.1, 0.5, 1])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-16", + "metadata": {}, + "source": [ + "**Excitation transport under uniform relaxation.** Without noise, the excitation\n", + "spreads outward and forms an interference pattern. Increasing the relaxation\n", + "rate suppresses occupation at later times and more distant sites. At $\\gamma=4$,\n", + "most of the excitation is lost before it travels far from the center.\n", + "\n", + "The fading pattern does not imply slower propagation. Conditioned on no jump,\n", + "the excitation follows the noiseless spatial dynamics because this Hamiltonian\n", + "conserves excitation and the loss rate is uniform. Site-dependent relaxation or\n", + "a different channel can change that profile. This time window mainly shows\n", + "outward propagation; longer runs can also show reflections from the open ends.\n", + "\n", + "## 8. Check the total excitation\n", + "\n", + "The heatmaps combine spreading with loss. Summing occupation over sites removes\n", + "the spatial information and isolates the survival probability. Since the initial\n", + "state has exactly one excitation and every site has the same relaxation rate,\n", + "the ensemble mean obeys\n", + "\n", + "$$\n", + "N(t)=\\sum_i\\langle n_i(t)\\rangle=e^{-\\gamma t}.\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cell-17", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:34:59.516247\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "total_excitation = occupations.sum(axis=1)\n", + "fine_times = np.linspace(times[0], times[-1], 200)\n", + "colors = [\"0.2\", \"#0072B2\", \"#009E73\", \"#D55E00\"]\n", + "fig, ax = plt.subplots(figsize=(6.2, 3.2))\n", + "for rate, values, color in zip(rates, total_excitation, colors, strict=True):\n", + " ax.plot(times, values, \"o-\", color=color, markersize=4,\n", + " label=\"Noiseless\" if rate == 0 else rf\"$\\gamma={rate:g}$\")\n", + " ax.plot(fine_times, np.exp(-rate * fine_times), \"--\", color=color,\n", + " linewidth=1.1, alpha=0.75)\n", + "ax.set(xlabel=r\"Time $t$\", ylabel=r\"Total excitation $N(t)$\",\n", + " xlim=(0, 3), ylim=(0, 1.08), xticks=[0, 1, 2, 3])\n", + "ax.legend(ncols=4, loc=\"lower center\", bbox_to_anchor=(0.5, 1.02))\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-18", + "metadata": {}, + "source": [ + "**Excitation survival compared with the decay law.** Markers show the YAQS\n", + "estimates, and dashed lines show $e^{-\\gamma t}$. At $t=3$, the exact survival\n", + "probabilities are one without noise and about $0.22$, $0.011$, and\n", + "$6\\times10^{-6}$ for the three nonzero rates.\n", + "\n", + "Each noisy trajectory either retains its excitation or loses it to a jump. With\n", + "only 32 trajectories, the estimated survival fraction changes in steps and may\n", + "reach zero while the exact mean is still positive. Finite sampling can also make\n", + "curves for different rates cross. Increase `num_traj` to reduce these\n", + "fluctuations; the sampling error decreases as $1/\\sqrt{\\mathtt{num\\_traj}}$.\n", + "\n", + "Together, the two figures distinguish loss from redistribution along the chain.\n", + "Uniform relaxation reduces the chance of finding the excitation, while surviving\n", + "excitations retain the coherent transport pattern. This conclusion and the decay\n", + "law rely on uniform loss and excitation-conserving dynamics. The noiseless total\n", + "also checks conservation, but that check alone cannot establish the accuracy of\n", + "the spatial dynamics.\n", + "\n", + "## Accuracy and other options\n", + "\n", + "Before using a larger or more demanding model, check convergence separately:\n", + "\n", + "| Change | What to check |\n", + "| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |\n", + "| Increase `num_traj` | Whether noisy observables stabilize within sampling uncertainty. |\n", + "| Reduce `dt` at fixed duration | Whether the dynamics stabilize with a finer evolution step. |\n", + "| Use `balanced` or `accurate` | Whether tighter truncation and solver settings change the observables. Keep an explicit trajectory budget when comparing numerical settings. |\n", + "| Increase `length` or `elapsed_time` | Whether boundaries, entanglement growth, and runtime affect the question being studied. |\n", + "\n", + "The default MPS evolution uses TDVP updates. To use the BUG integrator, import\n", + "`EvolutionMode` from `mqt.yaqs` and set `evolution_mode=EvolutionMode.BUG` in\n", + "`AnalogSimParams`. For TDVP, `tdvp_sweeps` adds unitary substeps without\n", + "changing the noise timestep. See {doc}`simulation_parameters` for these advanced\n", + "choices.\n", + "\n", + "## Related topics\n", + "\n", + "- {doc}`realistic_noise_models` — other channels, custom jumps, and static\n", + " disorder\n", + "- {doc}`digital_analog_simulation` — combine analog evolution and digital\n", + " operations\n", + "- {doc}`hamiltonians` — built-in, custom, hardware, and time-dependent\n", + " Hamiltonians\n", + "- {doc}`representation_comparison` — MPS, statevector, and density matrix\n", + " backends\n", + "- {ref}`noise-scheduled-jumps` — deterministic jumps at specified times\n", + "- {doc}`ensemble_evolution` — unitary ensemble correlations" + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 180, + "number_source_lines": true + }, + "source_map": [ + 10, + 37, + 42, + 59, + 65, + 78, + 89, + 111, + 119, + 135, + 141, + 153, + 159, + 172, + 186, + 193, + 237, + 261, + 276 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/characterization.ipynb b/docs/_outputs/examples/characterization.ipynb new file mode 100644 index 000000000..2d9321bed --- /dev/null +++ b/docs/_outputs/examples/characterization.ipynb @@ -0,0 +1,2544 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Probing Environmental Memory\n", + "\n", + "A quantum system can leave information in its environment and encounter that\n", + "information again later. To study this memory, we control one probe qubit,\n", + "interrupt its evolution with a measurement and preparation, then ask whether its\n", + "future responses still depend on the past. The interruption removes the probe's\n", + "direct link to its earlier state while the environment keeps evolving.\n", + "\n", + "We extend the coupling sweep in {doc}`quickstart` to explain the probing\n", + "schedule, the response matrix, and its spectrum. We then test memory persistence\n", + "under repeated resets and add dephasing in a short process-tensor example. The\n", + "examples use the standard YAQS installation and Matplotlib for plotting. Run the\n", + "cells in order in a notebook; for a script, use the entry-point guard in\n", + "{doc}`simulator_initialization`.\n", + "\n", + "## 1. Define the probe and its environment\n", + "\n", + "Site 0 is our probe qubit. Sites 1 and 2 form an environment that we do not\n", + "control or measure directly. All three spins evolve under the transverse-field\n", + "Ising Hamiltonian\n", + "\n", + "$$\n", + "H=-J(Z_0Z_1+Z_1Z_2)-g(X_0+X_1+X_2).\n", + "$$\n", + "\n", + "We fix $g=1$, use $\\hbar=1$, and vary $J$. This changes both the\n", + "probe–environment coupling and the bond within the environment. At $J=0$ the\n", + "probe is isolated, giving a reference with no environmental memory. The default\n", + "initial state is $|000\\rangle$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer\n", + "\n", + "length = 3\n", + "couplings = np.linspace(0, 1.5, 13)\n", + "params = AnalogSimParams(elapsed_time=0.5, dt=0.5, preset=\"fast\")\n", + "characterizer = MemoryCharacterizer(show_progress=False)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "The characterizer selects the state representation automatically: vectors for\n", + "small systems, and MPS for larger ones. Parallel execution remains enabled. The\n", + "documentation suppresses progress bars; omit `show_progress=False` to see them.\n", + "Memory characterization currently supports qubit Hamiltonians.\n", + "\n", + "(memory-theory)=\n", + "\n", + "## 2. Choose the probing schedule\n", + "\n", + "Use four interventions separated by evolution intervals of `dt=0.5`. YAQS also\n", + "evolves before the first intervention and after the last, giving five intervals\n", + "and a total duration of 2.5. With `cut=2`, the second intervention is the\n", + "**causal break**: measure a selected outcome and prepare a new probe state.\n", + "There is one control before the break and two controls after it.\n", + "\n", + "The default `intervention_style=\"haar\"` draws random single-qubit unitaries for\n", + "these controls. Past probes also choose the measurement at the break; future\n", + "probes choose its preparation. Keeping these choices separate lets us test\n", + "whether past settings affect the future through the environment." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cut=2: S_V=-0.0000, modes=1.000\n", + "Response matrix shape: (32, 8)\n" + ] + } + ], + "source": [ + "num_interventions = 4\n", + "cut = 2\n", + "anchor = characterizer.characterize(\n", + " Hamiltonian.ising(length, J=0.0, g=1.0),\n", + " params,\n", + " num_interventions=num_interventions,\n", + " cut=cut,\n", + " preset=\"quick\",\n", + " rng=np.random.default_rng(7),\n", + ")\n", + "\n", + "print(anchor.summary())\n", + "print(\"Response matrix shape:\", anchor.response_matrix(cut).shape)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "`preset=\"quick\"` selects eight past probes and eight future probes, giving 64\n", + "sequences. This preset sets the probe grid; the preset on `AnalogSimParams` sets\n", + "numerical accuracy. For Hamiltonian characterization, `dt` sets the interval\n", + "between interventions. `elapsed_time` does not set the full probing horizon.\n", + "Here it equals one interval so the simulation parameters have a valid time grid.\n", + "\n", + "For each past–future pair, YAQS retains the selected measurement outcome and\n", + "records the final probe's $(I,X,Y,Z)$ responses. It multiplies each conditional\n", + "response by the joint probability of the retained outcomes. The\n", + "**response matrix** $V$ places each history in a column and each future's four\n", + "response channels in consecutive rows. Its shape here is $(32,8)$, and its\n", + "identity rows contain the outcome probabilities. Keeping these probabilities\n", + "avoids treating a rare branch as though it occurred on every run.\n", + "\n", + "## 3. Sweep the coupling with the same probes\n", + "\n", + "Reuse `anchor` as `probe_set` so every coupling uses the same controls,\n", + "measurements, and preparations. Otherwise a change in the sampled probes could\n", + "be confused with a change in the environment." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "memories = [anchor]\n", + "for coupling in couplings[1:]:\n", + " memory = characterizer.characterize(\n", + " Hamiltonian.ising(length, J=float(coupling), g=1.0),\n", + " params,\n", + " num_interventions=num_interventions,\n", + " cut=cut,\n", + " probe_set=anchor,\n", + " )\n", + " memories.append(memory)\n", + "\n", + "mode_weights = []\n", + "for memory in memories:\n", + " spectrum = memory.singular_values(cut)\n", + " mode_weights.append(spectrum**2 / np.sum(spectrum**2))\n", + "entropies = np.array([memory.entropy(cut) for memory in memories])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "The singular values $s_k$ describe independent combinations of past settings and\n", + "future responses. Their normalized squared weights and entropy are\n", + "\n", + "$$\n", + "p_k=\\frac{s_k^2}{\\sum_j s_j^2},\\qquad\n", + "S_V=-\\sum_k p_k\\ln p_k.\n", + "$$\n", + "\n", + "`singular_values(cut)` returns the spectrum retained for this entropy, after\n", + "removing a numerical tail with relative squared weight at most $10^{-12}$.\n", + "`singular_values_full(cut)` returns every compact-SVD value. The entropy uses\n", + "natural logarithms, and `memory.modes(cut)` gives the effective mode number\n", + "$R=\\exp(S_V)$. One retained mode gives $S_V=0$ and $R=1$.\n", + "\n", + "## 4. Read the spectrum and entropy\n", + "\n", + "The spectrum shows how coupling redistributes the response among modes. The\n", + "entropy summarizes this spread, allowing us to compare the same probing\n", + "experiment across the coupling sweep." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:35:10.637477\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " Mode index\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 1\n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 1\n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " R\n", + " e\n", + " t\n", + " a\n", + " i\n", + " n\n", + " e\n", + " d\n", + "  \n", + " m\n", + " o\n", + " d\n", + " e\n", + "  \n", + " w\n", + " e\n", + " i\n", + " g\n", + " h\n", + " t\n", + "  \n", + " p\n", + " k\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a)\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " C\n", + " o\n", + " u\n", + " p\n", + " l\n", + " i\n", + " n\n", + " g\n", + "  \n", + " /\n", + " J\n", + " g\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.05\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.10\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.15\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.20\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.25\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.30\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " M\n", + " e\n", + " m\n", + " o\n", + " r\n", + " y\n", + "  \n", + " e\n", + " n\n", + " t\n", + " r\n", + " o\n", + " p\n", + " y\n", + "  \n", + "  \n", + " (\n", + " n\n", + " a\n", + " t\n", + " s\n", + " )\n", + " S\n", + " V\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b)\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " C\n", + " o\n", + " u\n", + " p\n", + " l\n", + " i\n", + " n\n", + " g\n", + "  \n", + " /\n", + " J\n", + " g\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib.colors import LinearSegmentedColormap, Normalize\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIXGeneral\"],\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 10,\n", + " \"axes.labelsize\": 11,\n", + " \"axes.linewidth\": 0.8,\n", + " \"xtick.direction\": \"in\",\n", + " \"ytick.direction\": \"in\",\n", + " \"svg.fonttype\": \"none\",\n", + "})\n", + "red_map = LinearSegmentedColormap.from_list(\"coupling_reds\", plt.colormaps[\"Reds\"](np.linspace(0.3, 0.95, 100)))\n", + "norm = Normalize(couplings[0], couplings[-1])\n", + "fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.8), layout=\"constrained\")\n", + "for coupling, weights in zip(couplings, mode_weights, strict=True):\n", + " axes[0].semilogy(np.arange(1, len(weights) + 1), weights, \"o-\", color=red_map(norm(coupling)), lw=1.3, ms=3)\n", + "axes[0].set(xlabel=\"Mode index\", ylabel=r\"Retained mode weight $p_k$\", ylim=(1e-13, 2), xticks=[1, 2, 4, 6, 8])\n", + "fig.colorbar(plt.cm.ScalarMappable(norm=norm, cmap=red_map), ax=axes[0], label=r\"Coupling $J/g$\", ticks=[0, 0.5, 1, 1.5], fraction=0.05, pad=0.03)\n", + "axes[1].fill_between(couplings, entropies, color=red_map(0.35), alpha=0.3)\n", + "axes[1].plot(couplings, entropies, \"o-\", color=red_map(0.95), lw=2.2, ms=4, markerfacecolor=\"white\")\n", + "axes[1].set(xlabel=r\"Coupling $J/g$\", ylabel=r\"Memory entropy $S_V$ (nats)\", xlim=(-0.03, 1.53), ylim=(-0.01, 0.34), xticks=[0, 0.5, 1, 1.5])\n", + "for label, ax in zip((\"(a)\", \"(b)\"), axes, strict=True):\n", + " ax.text(0.02, 1.03, label, transform=ax.transAxes, va=\"bottom\", fontweight=\"bold\")\n", + " ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "**Coupling creates additional response modes, with a peak inside this sweep.**\n", + "(a) Darker curves show larger $J$. The uncoupled probe has one retained mode;\n", + "coupled dynamics distribute weight among additional modes. (b) The entropy\n", + "reaches a maximum near $J/g=1.4$, then decreases. Stronger coupling does not\n", + "imply a larger entropy for a fixed schedule and probe grid.\n", + "\n", + "These weights describe the responses accessible through the chosen experiment.\n", + "They are not environment populations or Schmidt weights of a mixed state. A\n", + "small entropy means that a few modes dominate these responses; it does not prove\n", + "that every possible experiment would find the environment memoryless. Changing\n", + "the interval, temporal cut, or controls can reveal different memory. The\n", + "spectrum alone also does not certify that the memory is quantum rather than\n", + "classical.\n", + "\n", + "## Further experiments\n", + "\n", + "(reset-delay)=\n", + "\n", + "### How long does memory persist under resets?\n", + "\n", + "A causal break interrupts the probe once. Repeated resets ask whether the\n", + "selected histories remain distinguishable after a longer interruption. With\n", + "`delay=N`, YAQS measures the selected history outcome and prepares $|0\\rangle$,\n", + "inserts $N$ selected-zero measure–prepare resets, then measures zero once more\n", + "before the sampled future preparation. The environment evolves between all these\n", + "interventions." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": {}, + "outputs": [], + "source": [ + "hamiltonian = Hamiltonian.ising(length, J=1.0, g=1.0)\n", + "delays = np.arange(7)\n", + "delay_memories = []\n", + "for delay in delays:\n", + " delay_memories.append(characterizer.characterize(\n", + " hamiltonian,\n", + " params,\n", + " num_interventions=num_interventions,\n", + " cut=cut,\n", + " delay=int(delay),\n", + " probe_set=anchor,\n", + " ))\n", + "delay_entropies = [memory.entropy(cut) for memory in delay_memories]" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "Every integer delay, including zero, uses separate boundary interventions. The\n", + "sequence has `num_interventions + delay + 1` interventions and one more\n", + "evolution interval. Omitting `delay` uses the standard single causal break, so\n", + "the ordinary result and the `delay=0` result have different schedules." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:35:14.540059\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " Selected-zero reset slots\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.05\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.10\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.15\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.20\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.25\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.30\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " M\n", + " e\n", + " m\n", + " o\n", + " r\n", + " y\n", + "  \n", + " e\n", + " n\n", + " t\n", + " r\n", + " o\n", + " p\n", + " y\n", + "  \n", + "  \n", + " (\n", + " n\n", + " a\n", + " t\n", + " s\n", + " )\n", + " S\n", + " V\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(4.5, 2.6), layout=\"constrained\")\n", + "ax.plot(delays, delay_entropies, \"o-\", color=\"#225c80\", lw=2, ms=5, markerfacecolor=\"white\")\n", + "ax.fill_between(delays, delay_entropies, color=\"#225c80\", alpha=0.12)\n", + "ax.set(xlabel=\"Selected-zero reset slots\", ylabel=r\"Memory entropy $S_V$ (nats)\", xticks=delays, ylim=(0, None))\n", + "ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "**The conditioned memory varies nonmonotonically with reset delay.** The finite\n", + "spin environment continues to evolve and can return information to the probe.\n", + "These resets retain particular outcomes rather than averaging over every\n", + "measurement result. Their joint probability contributes to $V$, so this is a\n", + "conditioned persistence experiment. It does not establish an all-outcome memory\n", + "length. Explicit `delay` is supported for Hamiltonian characterization.\n", + "\n", + "### What changes when we add dephasing?\n", + "\n", + "The Hamiltonian `characterize` path does not accept a `NoiseModel`. To include\n", + "Markovian noise, first reconstruct a **dense process tensor**, which records the\n", + "response to interventions, then pass that tensor to `characterize`. We use two\n", + "spins and one causal break to keep this exhaustive reconstruction small. Two\n", + "evolution intervals of 0.5 surround the break; the probe and its one-spin\n", + "environment retain $J=g=1$." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dephasing rate 0: cut=1: S_V=0.0320, modes=1.033\n", + "Dephasing rate 1: cut=1: S_V=0.0035, modes=1.004\n", + "Dephasing rate 4: cut=1: S_V=0.0002, modes=1.000\n" + ] + } + ], + "source": [ + "from mqt.yaqs import NoiseModel\n", + "\n", + "short_hamiltonian = Hamiltonian.ising(2, J=1.0, g=1.0)\n", + "noise_params = AnalogSimParams(elapsed_time=0.5, dt=0.025, preset=\"fast\", random_seed=7)\n", + "dephasing_rates = [0.0, 1.0, 4.0]\n", + "process_tensors = []\n", + "noise_memories = []\n", + "short_anchor = None\n", + "for rate in dephasing_rates:\n", + " noise = None if rate == 0 else NoiseModel([{\"name\": \"pauli_z\", \"sites\": [0], \"strength\": rate}])\n", + " process = characterizer.build_process_tensor(\n", + " short_hamiltonian,\n", + " noise_params,\n", + " timesteps=[0.5, 0.5],\n", + " return_type=\"dense\",\n", + " noise_model=noise,\n", + " num_trajectories=512,\n", + " )\n", + " memory = characterizer.characterize(\n", + " process,\n", + " cut=1,\n", + " preset=\"quick\",\n", + " probe_set=short_anchor,\n", + " rng=np.random.default_rng(7),\n", + " )\n", + " short_anchor = memory if short_anchor is None else short_anchor\n", + " process_tensors.append(process)\n", + " noise_memories.append(memory)\n", + "\n", + "for rate, memory in zip(dephasing_rates, noise_memories, strict=True):\n", + " print(f\"Dephasing rate {rate:g}: {memory.summary()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "Here `strength=rate` is a Lindblad rate, with jump operator\n", + "$L=\\sqrt{\\gamma}\\,Z_0$ and dissipator $\\gamma(Z_0\\rho Z_0-\\rho)$. `timesteps`\n", + "defines the two evolution intervals, while `noise_params.dt` sets the\n", + "integration step within them. Each of the 16 tomography sequences averages 512\n", + "trajectories for nonzero noise. The noiseless reconstruction uses one." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cell-15", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:35:17.887365\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " Mode index\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " −\n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 0\n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " R\n", + " e\n", + " t\n", + " a\n", + " i\n", + " n\n", + " e\n", + " d\n", + "  \n", + " m\n", + " o\n", + " d\n", + " e\n", + "  \n", + " w\n", + " e\n", + " i\n", + " g\n", + " h\n", + " t\n", + "  \n", + " p\n", + " k\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a)\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " γ\n", + " g\n", + " /\n", + " =\n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " γ\n", + " g\n", + " /\n", + " =\n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " γ\n", + " g\n", + " /\n", + " =\n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " D\n", + " e\n", + " p\n", + " h\n", + " a\n", + " s\n", + " i\n", + " n\n", + " g\n", + "  \n", + " r\n", + " a\n", + " t\n", + " e\n", + "  \n", + " /\n", + " γ\n", + " g\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.01\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.02\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.03\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " M\n", + " e\n", + " m\n", + " o\n", + " r\n", + " y\n", + "  \n", + " e\n", + " n\n", + " t\n", + " r\n", + " o\n", + " p\n", + " y\n", + "  \n", + "  \n", + " (\n", + " n\n", + " a\n", + " t\n", + " s\n", + " )\n", + " S\n", + " V\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b)\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.6), layout=\"constrained\")\n", + "noise_colors = [\"#225c80\", \"#b78730\", \"#bb563b\"]\n", + "for rate, memory, color in zip(dephasing_rates, noise_memories, noise_colors, strict=True):\n", + " spectrum = memory.singular_values(1)\n", + " weights = spectrum**2 / np.sum(spectrum**2)\n", + " axes[0].semilogy(np.arange(1, len(weights) + 1), weights, \"o-\", color=color, ms=4, lw=1.6, label=rf\"$\\gamma/g={rate:g}$\")\n", + "axes[0].set(xlabel=\"Mode index\", ylabel=r\"Retained mode weight $p_k$\", xticks=[1, 2, 3, 4], ylim=(1e-8, 2))\n", + "axes[0].legend(frameon=False, fontsize=9, loc=\"lower left\")\n", + "axes[1].bar(np.arange(3), [memory.entropy(1) for memory in noise_memories], color=noise_colors, width=0.6)\n", + "axes[1].set(xlabel=r\"Dephasing rate $\\gamma/g$\", ylabel=r\"Memory entropy $S_V$ (nats)\", xticks=np.arange(3), xticklabels=[\"0\", \"1\", \"4\"])\n", + "for label, ax in zip((\"(a)\", \"(b)\"), axes, strict=True):\n", + " ax.text(0.02, 1.03, label, transform=ax.transAxes, va=\"bottom\", fontweight=\"bold\")\n", + " ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-16", + "metadata": {}, + "source": [ + "**Dephasing reduces the weight outside the leading response mode.** (a) The\n", + "leading mode gains relative weight as the dephasing rate increases. (b) The\n", + "sampled entropy falls. These values describe the two-spin, one-break schedule\n", + "and should not be compared directly with the earlier four-intervention sweep.\n", + "Small spectral weights can reflect trajectory sampling error; they do not all\n", + "establish resolved physical modes. Increase the trajectory count and refine the\n", + "integration step before interpreting the smallest weights.\n", + "\n", + "Coupling, resets, and added dephasing answer different questions about the same\n", + "physical issue: which traces of earlier probe choices can affect later\n", + "responses? Keep the probes and schedule fixed when comparing models, and check\n", + "whether the observed spectrum persists as numerical and sampling accuracy\n", + "improve.\n", + "\n", + "## Further options\n", + "\n", + "### Probe coverage and representations\n", + "\n", + "`preset=\"balanced\"` uses a $32\\times32$ grid and `\"accurate\"` uses\n", + "$128\\times128$. Set `n_pasts` and `n_futures` to choose counts explicitly.\n", + "Larger grids test more controls; they are not extra trajectories of the same\n", + "experiment. Check probe coverage as well as evolution accuracy.\n", + "\n", + "Use `intervention_style=\"clifford\"` for random single-qubit Clifford controls,\n", + "or `\"measure_prepare\"` for rank-one measurement and preparation on every leg.\n", + "The default `\"haar\"` uses random unitaries away from the cut. The choice changes\n", + "what memory the experiment can resolve.\n", + "\n", + "Pass `cuts=[...]` or `cuts=\"all\"` to compare temporal cuts. Each cut needs its\n", + "own probe geometry, so `probe_set` reuse is restricted to a single cut.\n", + "`representation=\"auto\"` on `MemoryCharacterizer` selects vectors up to ten\n", + "qubits and MPS above that size. Set `\"vector\"` or `\"mps\"` explicitly when\n", + "needed. `initial_psi` replaces the default all-zero state for Hamiltonian\n", + "characterization. See {class}`~mqt.yaqs.MemoryCharacterizer` for execution and\n", + "accuracy options.\n", + "\n", + "### Inspecting response modes\n", + "\n", + "Use `memory.response_matrix(cut)` to inspect $V$. The full compact-SVD factors\n", + "from `left_singular_vectors`, `singular_values_full`, and\n", + "`right_singular_vectors` satisfy $V=U\\operatorname{diag}(s)W^\\dagger$. The\n", + "columns of $U$ describe future responses, while the columns of $W$ combine\n", + "histories. Directions paired with an unresolved tail are not resolved modes, and\n", + "vectors inside a degenerate singular subspace are not unique.\n", + "\n", + "### Process tensors and temporal entanglement\n", + "\n", + "`build_process_tensor` defaults to direct MPO construction for noiseless models.\n", + "Use `return_type=\"dense\"` for added noise, as above. Both constructions grow\n", + "with $16^k$ intervention sequences or histories, so reserve them for short\n", + "horizons. Direct construction retains all branches by default; `compress_every`\n", + "controls an accumulation batch, not the total number of histories.\n", + "\n", + "A process tensor also provides `compute_temporal_entropy(cut)`, `qmi`, and\n", + "`cmi`. These describe the multi-time process, while $S_V$ describes the sampled\n", + "probe responses. They are distinct quantities. Reuse the noiseless short-horizon\n", + "tensor to compute its temporal operator-Schmidt entropy:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cell-17", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Temporal entropy S_PT: 0.0762\n", + "Response entropy S_V: 0.0320\n" + ] + } + ], + "source": [ + "temporal = process_tensors[0].compute_temporal_entropy(1)\n", + "print(f\"Temporal entropy S_PT: {temporal['entropy']:.4f}\")\n", + "print(f\"Response entropy S_V: {noise_memories[0].entropy(1):.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-18", + "metadata": {}, + "source": [ + "Both entropies use natural logarithms. Their values differ because they describe\n", + "different objects. The MPO implementations of these diagnostics currently\n", + "densify the tensor. Dense storage alone takes 64 MiB at five intervention legs\n", + "and 1 GiB at six, before analysis workspace. Operational probing of an MPO\n", + "process tensor does not require this conversion.\n", + "\n", + "```{warning}\n", + "A finite `max_bond_dim` in direct process-tensor construction enables an\n", + "experimental approximation that can violate positivity and causal normalization.\n", + "Keep the supported default `None` for scientific references. Noisy tomography\n", + "also has finite-sample error; validate reconstructed responses before using them\n", + "as a reference.\n", + "```\n", + "\n", + "For predicting dynamics under new controls with a trained model, see\n", + "{doc}`memory_surrogate`. Surrogate characterization requires `initial_rho`, the\n", + "site-0 state after initial evolution and before the first intervention. Use the\n", + "reference process tensor's `initial_rho` when the surrogate was trained against\n", + "that tensor, and reuse the same `probe_set` for a direct comparison." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 42, + 51, + 73, + 87, + 109, + 126, + 148, + 180, + 209, + 223, + 230, + 238, + 256, + 288, + 296, + 312, + 372, + 376 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/circuit_observables.ipynb b/docs/_outputs/examples/circuit_observables.ipynb new file mode 100644 index 000000000..336d1b1bb --- /dev/null +++ b/docs/_outputs/examples/circuit_observables.ipynb @@ -0,0 +1,5196 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Observable-Based Circuit Simulation\n", + "\n", + "The excitation transport in {doc}`analog_simulation` can also be simulated with\n", + "a quantum circuit. We use the same **20-site XY chain**, localized excitation,\n", + "and relaxation rates, then replace continuous Hamiltonian evolution with short\n", + "sequences of exchange gates. Sampling observables between these sequences lets\n", + "us reconstruct the occupation heatmaps and compare digital and analog dynamics.\n", + "\n", + "The example uses the standard YAQS installation and public interfaces. Run the\n", + "cells in order in a notebook; in a script, use the entry-point guard in\n", + "{doc}`simulator_initialization`. For bitstring counts rather than expectation\n", + "values, see {doc}`circuit_shots`.\n", + "\n", + "## 1. Turn the XY Hamiltonian into gates\n", + "\n", + "The Hamiltonian exchanges excitations between neighboring sites,\n", + "\n", + "$$\n", + "H=-\\frac{1}{2}\\sum_{i=0}^{L-2}(X_iX_{i+1}+Y_iY_{i+1}).\n", + "$$\n", + "\n", + "As in the analog guide, we set the hopping amplitude to one and use $\\hbar=1$.\n", + "An exchange gate on sites $i$ and $i+1$ implements their contribution to the\n", + "evolution. Qiskit's `XXPlusYYGate(-2 * duration)` gives\n", + "$\\exp[+i\\,\\mathtt{duration}(XX+YY)/2]$, with the sign set by this Hamiltonian.\n", + "\n", + "Gates on overlapping bonds do not commute. We approximate a time step with half\n", + "a step on even bonds, a full step on odd bonds, then another half step on even\n", + "bonds. This symmetric Trotter formula approaches the Hamiltonian dynamics as the\n", + "step decreases. Each exchange gate preserves excitation number, including when\n", + "noise acts between gates." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit.library import XXPlusYYGate\n", + "\n", + "\n", + "def xy_circuit(length, dt, steps):\n", + " \"\"\"Build a symmetric XY Trotter circuit and count gate exposures per step.\"\"\"\n", + " step_circuit = QuantumCircuit(length)\n", + " gate_exposures = np.zeros(length, dtype=int)\n", + " for parity, fraction in ((0, 0.5), (1, 1.0), (0, 0.5)):\n", + " for site in range(parity, length - 1, 2):\n", + " step_circuit.append(XXPlusYYGate(-2 * dt * fraction), [site, site + 1])\n", + " gate_exposures[[site, site + 1]] += 1\n", + "\n", + " circuit = QuantumCircuit(length)\n", + " for step in range(steps):\n", + " circuit.compose(step_circuit, inplace=True)\n", + " if step < steps - 1:\n", + " circuit.barrier(label=\"SAMPLE_OBSERVABLES\")\n", + " return circuit, gate_exposures\n", + "\n", + "\n", + "length = 20\n", + "dt = 0.25\n", + "steps = 12\n", + "circuit, gate_exposures = xy_circuit(length, dt, steps)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "The circuit represents evolution to $t=3$. Labelled barriers separate the\n", + "Trotter steps so YAQS can sample observables at the same 13 times as the analog\n", + "guide. `gate_exposures` counts each site's two-qubit gates in one step; we will\n", + "use these counts to match the relaxation rate.\n", + "\n", + "## 2. Prepare the state and observables\n", + "\n", + "Start with one excitation at site 10. Measuring $Z_i$ at every site gives the\n", + "occupation through $\\langle n_i\\rangle=(1-\\langle Z_i\\rangle)/2$." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import DigitalSimParams, NoiseModel, Observable, Simulator, State\n", + "\n", + "center = length // 2\n", + "basis = \"0\" * center + \"1\" + \"0\" * (length - center - 1)\n", + "state = State(length, initial=\"basis\", basis_string=basis)\n", + "observables = [Observable(\"z\", site) for site in range(length)]\n", + "params = DigitalSimParams(\n", + " observables=observables,\n", + " sample_layers=True,\n", + " num_traj=16,\n", + " preset=\"fast\",\n", + " random_seed=7,\n", + ")\n", + "sim = Simulator(show_progress=False)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "`sample_layers=True` records observables at the circuit start, at each barrier\n", + "labelled `SAMPLE_OBSERVABLES`, and after the final gates. Barrier labels are\n", + "case-insensitive; unlabelled barriers do not trigger sampling. These checkpoints\n", + "evaluate expectations without collapsing the state, unlike hardware mid-circuit\n", + "measurements.\n", + "\n", + "The noisy results average 16 trajectories, while a noiseless run uses one.\n", + "Parallel execution remains enabled. The documentation suppresses progress bars;\n", + "omit `show_progress=False` to see them. The preset controls numerical\n", + "tolerances, while `num_traj` controls sampling uncertainty.\n", + "\n", + "## 3. Match relaxation to circuit steps\n", + "\n", + "The analog model uses a uniform relaxation rate $\\gamma$ per unit of physical\n", + "time. Circuit noise instead acts for one unit of noise time after each gate on\n", + "two or more qubits, using only processes supported on that gate's qubits.\n", + "Single-qubit gates and idle sites receive no noise.\n", + "\n", + "Using the same numerical strength for every gate would make damping depend on\n", + "the number of gates rather than the represented duration. For a site involved in\n", + "$m_i$ gates per Trotter step, set its circuit strength to\n", + "\n", + "$$\n", + "\\mathtt{strength}_i=\\frac{\\gamma\\,\\Delta t}{m_i}.\n", + "$$\n", + "\n", + "Here the end sites encounter two gates per step and interior sites encounter\n", + "three. Dividing by these counts gives each site a total relaxation exposure of\n", + "$\\gamma\\Delta t$ per step. Noise is still interleaved with the gates, so the\n", + "finite-step evolution can differ from the continuous analog model. This rate\n", + "mapping describes a digital approximation to that model; hardware gate noise\n", + "should instead follow the device and its compiled operations." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "rates = [0.0, 0.5, 1.5, 4.0]\n", + "results = {}\n", + "for rate in rates:\n", + " noise = None if rate == 0 else NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site],\n", + " \"strength\": rate * dt / gate_exposures[site]}\n", + " for site in range(length)\n", + " ])\n", + " results[rate] = sim.run(state, circuit, params, noise)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "## 4. Read the sampled dynamics\n", + "\n", + "`expectation_values` contains one NumPy array per observable. The observable\n", + "order follows the supplied list, and each array follows checkpoint order.\n", + "Combine the arrays and convert $Z_i$ to occupation:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(4, 20, 13)\n" + ] + } + ], + "source": [ + "times = dt * np.arange(steps + 1)\n", + "occupations = np.stack([\n", + " (1 - np.asarray(results[rate].expectation_values).real) / 2\n", + " for rate in rates\n", + "])\n", + "print(occupations.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "The shape is `(4, 20, 13)`: relaxation rates by sites by checkpoints. Digital\n", + "results can store complex values; `.real` selects the real expectation of these\n", + "Hermitian observables. `result.trajectories` retains the per-trajectory data.\n", + "\n", + "A standalone circuit has `result.times=None` because its gates do not define\n", + "physical durations. The `times` array above assigns physical times from our\n", + "Trotter construction. For final observables only, omit `sample_layers=True`;\n", + "each observable then has one sample. To collect counts too, supply `shots` and\n", + "YAQS distributes that total budget across the noisy observable trajectories.\n", + "\n", + "## 5. Reconstruct the transport heatmaps\n", + "\n", + "The following panels use the analog guide's time window, noise strengths, and\n", + "shared square-root color scale. They show absolute occupation, without\n", + "normalizing by the remaining excitation." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:36:06.132278\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib.colors import PowerNorm\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIXGeneral\"],\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 11,\n", + " \"axes.labelsize\": 11,\n", + " \"axes.linewidth\": 0.7,\n", + " \"xtick.labelsize\": 10,\n", + " \"ytick.labelsize\": 10,\n", + " \"xtick.direction\": \"in\",\n", + " \"ytick.direction\": \"in\",\n", + " \"xtick.top\": True,\n", + " \"ytick.right\": True,\n", + " \"legend.fontsize\": 9,\n", + " \"legend.frameon\": False,\n", + " \"lines.linewidth\": 1.6,\n", + " \"figure.constrained_layout.use\": True,\n", + " \"savefig.dpi\": 180,\n", + "})\n", + "fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.4), sharex=True, sharey=True)\n", + "for ax, values, rate, panel in zip(axes.flat, occupations, rates, \"abcd\", strict=True):\n", + " image = ax.pcolormesh(times, np.arange(length), values, shading=\"auto\",\n", + " cmap=\"cividis\", norm=PowerNorm(0.5, vmin=0, vmax=1),\n", + " rasterized=True)\n", + " label = \"Noiseless\" if rate == 0 else rf\"$\\gamma={rate:g}$\"\n", + " ax.set_title(f\"({panel}) {label}\", loc=\"left\", fontsize=11)\n", + " ax.set(xticks=[0, 1, 2, 3], yticks=[0, 5, 10, 15, 19])\n", + "for ax in axes[-1]:\n", + " ax.set_xlabel(r\"Represented time $t$\")\n", + "for ax in axes[:, 0]:\n", + " ax.set_ylabel(r\"Site $i$\")\n", + "fig.colorbar(image, ax=axes.ravel().tolist(),\n", + " label=r\"Occupation $\\langle n_i\\rangle$\", ticks=[0, 0.1, 0.5, 1])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "**Digital reconstruction of excitation transport and loss.** Exchange gates\n", + "spread the initial excitation along the chain, while increasing relaxation\n", + "suppresses occupation at later times. The broad patterns reproduce the analog\n", + "example, with differences from Trotter splitting and finite trajectory sampling.\n", + "\n", + "The heatmaps alone do not tell us how close the circuit is to Hamiltonian\n", + "evolution. To separate the approximation in the gates from sampling noise, we\n", + "next compare the noiseless circuit with an analog reference and a finer circuit.\n", + "\n", + "## 6. Compare with analog evolution\n", + "\n", + "Run the same Hamiltonian without noise, then halve the circuit step while\n", + "keeping the total duration fixed. Select every second checkpoint of the finer\n", + "circuit to compare at the original sampling times." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import AnalogSimParams, Hamiltonian\n", + "\n", + "hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0)\n", + "analog_params = AnalogSimParams(\n", + " observables=observables, elapsed_time=steps * dt, dt=dt, preset=\"fast\",\n", + ")\n", + "analog = sim.run(state, hamiltonian, analog_params)\n", + "analog_occupation = (1 - np.asarray(analog.expectation_values)) / 2\n", + "\n", + "fine_circuit, _ = xy_circuit(length, dt / 2, steps * 2)\n", + "fine_result = sim.run(state, fine_circuit, params)\n", + "fine_occupation = (1 - np.asarray(fine_result.expectation_values).real[:, ::2]) / 2" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "The first panel compares final occupation profiles. The second sums occupation\n", + "over sites and compares noisy circuit estimates with the analog model's decay\n", + "law, $N(t)=e^{-\\gamma t}$. This law holds because the initial state contains one\n", + "excitation, the Hamiltonian conserves excitation, and relaxation is uniform." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:36:06.757573\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.2))\n", + "ax = axes[0]\n", + "for values, label, style, color in (\n", + " (analog_occupation, \"Analog\", \"-\", \"0.2\"),\n", + " (occupations[0], r\"Circuit $\\Delta t=0.25$\", \"o\", \"#0072B2\"),\n", + " (fine_occupation, r\"Circuit $\\Delta t=0.125$\", \"+\", \"#D55E00\"),\n", + "):\n", + " ax.plot(np.arange(length), values[:, -1], style, color=color,\n", + " label=label, markersize=4)\n", + "ax.set(xlabel=\"Site\", ylabel=r\"Final occupation $\\langle n_i\\rangle$\",\n", + " xticks=[0, 5, 10, 15, 19])\n", + "ax.set_title(\"(a) Noiseless transport\", loc=\"left\", fontsize=11)\n", + "ax.legend(loc=\"upper center\", fontsize=8)\n", + "\n", + "ax = axes[1]\n", + "colors = [\"0.2\", \"#0072B2\", \"#009E73\", \"#D55E00\"]\n", + "fine_times = np.linspace(0, times[-1], 200)\n", + "for rate, values, color in zip(rates, occupations, colors, strict=True):\n", + " ax.plot(times, values.sum(axis=0), \"o\", color=color, markersize=3,\n", + " label=\"Noiseless\" if rate == 0 else rf\"$\\gamma={rate:g}$\")\n", + " ax.plot(fine_times, np.exp(-rate * fine_times), \"--\", color=color, linewidth=1)\n", + "ax.set(xlabel=r\"Represented time $t$\", ylabel=r\"Total excitation $N(t)$\",\n", + " xlim=(0, 3), ylim=(0, 1.08), xticks=[0, 1, 2, 3])\n", + "ax.set_title(\"(b) Excitation survival\", loc=\"left\", fontsize=11)\n", + "ax.legend(ncols=2, loc=\"upper right\", bbox_to_anchor=(1, 0.9), fontsize=8)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "**Analog and digital transport compared at the same duration.** The final\n", + "noiseless profiles agree closely, and a smaller Trotter step reduces the\n", + "circuit's splitting error. Noisy survival estimates follow the uniform-loss\n", + "decay within the resolution of this 16-trajectory example; dashed lines show the\n", + "continuous-model reference.\n", + "\n", + "This comparison connects the two workflows through their physical model, rather\n", + "than equating a circuit's gate count with elapsed time. Check Trotter-step\n", + "convergence separately from MPS tolerances and trajectory uncertainty. For noisy\n", + "refinement, rebuild the circuit and rescale the strengths using the new step and\n", + "gate exposures. Finite-step noise splitting can also affect the spatial profile;\n", + "agreement of total excitation alone does not validate that profile.\n", + "\n", + "(circuit-qasm-inputs)=\n", + "\n", + "## 7. OpenQASM inputs\n", + "\n", + "Pass an OpenQASM 2 source string (or file path) directly to\n", + "{meth}`~mqt.yaqs.Simulator.run` instead of building a\n", + "{class}`qiskit.circuit.QuantumCircuit` in Python. Custom gate bodies declared in\n", + "the program are translated like any other Qiskit operation." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cell-15", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import DigitalSimParams\n", + "\n", + "qasm = \"\"\"\n", + "OPENQASM 2.0;\n", + "include \"qelib1.inc\";\n", + "\n", + "gate entangle a,b {\n", + " h a;\n", + " cx a,b;\n", + "}\n", + "\n", + "qreg q[2];\n", + "entangle q[0], q[1];\n", + "\"\"\"\n", + "\n", + "qasm_state = State(2, initial=\"zeros\")\n", + "qasm_result = sim.run(\n", + " qasm_state,\n", + " qasm,\n", + " DigitalSimParams(shots=128, max_bond_dim=4),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-16", + "metadata": {}, + "source": [ + "OpenQASM 3 requires `uv pip install mqt-yaqs[qasm3]`.\n", + "{class}`~mqt.yaqs.EquivalenceChecker` accepts the same path and string forms;\n", + "see {doc}`equivalence_checking`.\n", + "\n", + "## 8. Gate application modes\n", + "\n", + "`DigitalSimParams.gate_mode` selects how two-qubit gates are applied to the MPS.\n", + "The default `\"mpo\"` uses extended gate MPOs for long-range pairs; `\"tdvp\"` uses\n", + "a local TDVP window when an analytic generator is available. See\n", + "{doc}`simulation_parameters` for the available modes and\n", + "{ref}`circuit-custom-gates` for matrix-backed gates.\n", + "\n", + "Below, a long-range `cx` on qubits 0 and 2 is simulated noiselessly with both\n", + "modes:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cell-17", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'mpo': 0.0, 'tdvp': 0.0}\n" + ] + } + ], + "source": [ + "lr_qc = QuantumCircuit(3)\n", + "lr_qc.h(0)\n", + "lr_qc.cx(0, 2)\n", + "\n", + "lr_state = State(3, initial=\"zeros\")\n", + "z0_by_mode = {}\n", + "for mode in (\"mpo\", \"tdvp\"):\n", + " mode_params = DigitalSimParams(\n", + " observables=[Observable(\"z\", 0)],\n", + " num_traj=1,\n", + " gate_mode=mode,\n", + " max_bond_dim=8,\n", + " )\n", + " mode_result = sim.run(lr_state, lr_qc, mode_params)\n", + " z0_by_mode[mode] = float(np.real(mode_result.expectation_values[0][0]))\n", + "\n", + "print({mode: round(value, 4) for mode, value in z0_by_mode.items()})" + ] + }, + { + "cell_type": "markdown", + "id": "cell-18", + "metadata": {}, + "source": [ + "(circuit-custom-gates)=\n", + "\n", + "## 9. Supply custom gates\n", + "\n", + "Add a custom unitary to a Qiskit circuit with `UnitaryGate`. YAQS translates the\n", + "matrix automatically, so no gate registration is needed. This two-qubit example\n", + "applies a phase only to the $|11\\rangle$ component:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "cell-19", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.9605305+0.j]\n" + ] + } + ], + "source": [ + "from qiskit.circuit.library import UnitaryGate\n", + "\n", + "custom_unitary = np.diag([1, 1, 1, np.exp(0.4j)])\n", + "custom_circuit = QuantumCircuit(2)\n", + "custom_circuit.h([0, 1])\n", + "custom_circuit.append(UnitaryGate(custom_unitary), [0, 1])\n", + "\n", + "custom_params = DigitalSimParams(observables=[Observable(\"x\", 0)])\n", + "custom_sim = Simulator(show_progress=False)\n", + "custom_result = custom_sim.run(State(2, initial=\"zeros\"), custom_circuit, custom_params)\n", + "print(custom_result.expectation_values[0])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-20", + "metadata": {}, + "source": [ + "The final expectation is $\\langle X_0\\rangle=(1+\\cos 0.4)/2$, about 0.9605. The\n", + "matrix uses Qiskit's qubit ordering; YAQS converts it to its internal gate\n", + "layout. The same input works with `shots`, noise, and sampling checkpoints under\n", + "the circuit rules described above.\n", + "\n", + "Custom gate bodies in {ref}`circuit-qasm-inputs` follow the same translation\n", + "path. Unknown unitary operations use a matrix fallback on up to eight qubits;\n", + "decompose larger operations first. A matrix-backed gate has no analytic\n", + "generator: TDVP gate modes use direct local updates for adjacent pairs and the\n", + "MPO path for separated sites or larger gates. Keep `gate_mode=\"mpo\"` unless you\n", + "need another method.\n", + "\n", + ":::{dropdown} Supported instructions and gate translation\n", + "\n", + "Bind symbolic Qiskit parameters before simulation. YAQS translates known gate\n", + "names through its gate library; other operations must provide a unitary matrix\n", + "through Qiskit's `to_matrix()` or `Operator`. This also supports gates defined\n", + "by a reusable Qiskit circuit or an OpenQASM gate body. Use a distinct name for a\n", + "custom operation, since a name matching a built-in alias selects that built-in\n", + "implementation.\n", + "\n", + "Terminal measurements are removed before simulation; request `shots` for\n", + "readout. Measurements followed by further operations on the measured qubits are\n", + "unsupported. `reset`, `delay`, `store`, classical conditions, and control-flow\n", + "instructions are also unsupported. Ordinary barriers do not change the state;\n", + "barriers labelled `SAMPLE_OBSERVABLES` mark sampling points.\n", + "\n", + "Digital gates require qubit target sites. Idle sites can have other local\n", + "dimensions, but `gate_mode=\"swaps\"` cannot route through a non-qubit site.\n", + "\n", + "Built-in `ccx`, `ccz`, and `cswap` gates translate without decomposition.\n", + "Simulation applies gates on three or more qubits through an MPO, except\n", + "supported product generators such as `ccx` and `ccz` in TDVP modes. The\n", + "`\"swaps\"` mode also uses the MPO path for these larger gates.\n", + "\n", + "`EquivalenceChecker` accepts the same unitary and OpenQASM inputs. Gates on more\n", + "than two qubits require its `\"matrix\"` backend; decompose them before using the\n", + "`\"mpo\"` backend. See {doc}`equivalence_checking` for backend choice and\n", + "measurement restrictions. Translation details and the supported alias list are\n", + "in {mod}`~mqt.yaqs.digital.utils.dag_utils`.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} Low-level gate objects\n", + "\n", + "Application code should supply Qiskit circuits. For code that works directly\n", + "with YAQS gate kernels, `GateLibrary.custom` constructs a matrix-backed\n", + "{class}`~mqt.yaqs.core.libraries.gate_library.BaseGate`:\n", + "\n", + "```python\n", + "from mqt.yaqs.core.libraries.gate_library import GateLibrary\n", + "\n", + "gate = GateLibrary.custom(np.eye(4, dtype=complex))\n", + "gate.name = \"my_gate\"\n", + "gate.set_sites(0, 1)\n", + "```\n", + "\n", + "This constructor checks that the matrix is square with dimension $2^n$. The\n", + "caller must supply a finite unitary. The resulting fields are:\n", + "\n", + "| Field | Meaning |\n", + "| ------------- | -------------------------------------------------------------- |\n", + "| `matrix` | Gate matrix in YAQS gate order. |\n", + "| `interaction` | Number of target qubits, inferred from the matrix size. |\n", + "| `sites` | Target sites in their declared order. |\n", + "| `tensor` | Gate tensor; `set_sites` reshapes gates on two or more qubits. |\n", + "| `generator` | Optional local factors for a product-form TDVP generator. |\n", + "| `name` | Gate identifier. |\n", + "\n", + "In a manually supplied gate matrix, the first tensor factor acts on the first\n", + "declared site. Qiskit translation handles its different matrix convention\n", + "automatically. Creating this object does not register a new Qiskit gate or make\n", + "it a valid operator argument to `Simulator.run`.\n", + "\n", + "Built-in gates subclass `BaseGate` and prepare tensors and optional generators\n", + "in `set_sites`. See {class}`~mqt.yaqs.core.libraries.gate_library.CX` and\n", + "{class}`~mqt.yaqs.core.libraries.gate_library.CCX` for examples.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} Product generators for digital TDVP\n", + "\n", + "A TDVP-capable gate has one $2\\times2$ generator factor per target site. The\n", + "factors define a product $G$ whose exponential at evolution time one must\n", + "reproduce the gate, $U=\\exp(-iG)$. For example, a ZZ phase rotation has\n", + "$G=(\\theta Z/2)\\otimes Z$:\n", + "\n", + "```python\n", + "from scipy.linalg import expm\n", + "\n", + "from mqt.yaqs.core.libraries.gate_library import GateLibrary\n", + "\n", + "theta = 0.3\n", + "pauli_z = np.diag([1.0, -1.0])\n", + "generator_factors = [theta * pauli_z / 2, pauli_z]\n", + "phase_unitary = expm(-1j * np.kron(*generator_factors))\n", + "phase_gate = GateLibrary.custom(phase_unitary)\n", + "phase_gate.set_sites(0, 2)\n", + "phase_gate.generator = generator_factors\n", + "```\n", + "\n", + "The factors follow the declared `sites` order. YAQS places identities between\n", + "separated factors when constructing the generator MPO. The caller must check\n", + "that the full generator is Hermitian and reproduces the unitary; YAQS does not\n", + "verify that relation. This low-level assignment does not change how a Qiskit\n", + "`UnitaryGate` is translated.\n", + "\n", + "Digital generator evolution requires `tdvp_mode=\"2site\"`. `tdvp_sweeps` divides\n", + "the total generator time of one into substeps. TDVP gate application remains\n", + "approximate and can miss required bond growth; see {doc}`simulation_parameters`\n", + "for accuracy limits. Single-qubit gates always use direct contraction. For\n", + "implementation details, see\n", + "{func}`~mqt.yaqs.digital.digital_tjm.construct_generator_mpo`.\n", + "\n", + ":::\n", + "\n", + "## Related topics\n", + "\n", + "- {doc}`digital_analog_simulation` — combine digital operations with analog\n", + " evolution in one program\n", + "- {doc}`circuit_shots` — computational-basis shot histograms with\n", + " {class}`~mqt.yaqs.DigitalSimParams`\n", + "- {doc}`realistic_noise_models` — log-normal and other distributed noise\n", + " strengths\n", + "- {doc}`equivalence_checking` — verify that two circuits implement the same\n", + " unitary\n", + "- {doc}`quickstart` — minimal analog, circuit, and equivalence-check workflows" + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 44, + 71, + 83, + 98, + 133, + 143, + 151, + 158, + 176, + 217, + 234, + 247, + 254, + 282, + 306, + 328, + 345, + 363, + 373, + 385 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/circuit_shots.ipynb b/docs/_outputs/examples/circuit_shots.ipynb new file mode 100644 index 000000000..127c923ea --- /dev/null +++ b/docs/_outputs/examples/circuit_shots.ipynb @@ -0,0 +1,3564 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Shot-Based Circuit Simulation\n", + "\n", + "Relaxation changes which bitstrings a quantum circuit produces. We study this\n", + "change by preparing a **16-qubit graph state** and sampling its readout at\n", + "several damping strengths. Grouping the outcomes by the number of excited qubits\n", + "makes the loss of excitation visible without plotting all $2^{16}$ bitstrings.\n", + "\n", + "This guide extends the circuit-readout example in {doc}`quickstart` using the\n", + "standard YAQS installation. YAQS evolves a matrix product state (MPS) through\n", + "the circuit and samples the final state in the computational basis. Run the\n", + "cells in order in a notebook. For a script, use the `if __name__ == \"__main__\":`\n", + "guard shown in {doc}`simulator_initialization`.\n", + "\n", + "## 1. Prepare the circuit and initial state\n", + "\n", + "Start from $|0\\rangle^{\\otimes16}$, apply a Hadamard gate to each qubit, then\n", + "entangle neighboring qubits with CZ gates. These gates change relative phases\n", + "without changing computational-basis probabilities, so the ideal readout has\n", + "equal probability for every bitstring." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "from mqt.yaqs import State\n", + "\n", + "num_qubits = 16\n", + "circuit = QuantumCircuit(num_qubits)\n", + "circuit.h(range(num_qubits))\n", + "for site in range(num_qubits - 1):\n", + " circuit.cz(site, site + 1)\n", + "circuit.measure_all()\n", + "\n", + "state = State(num_qubits, initial=\"zeros\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "`State` uses an MPS by default. `shots` requests final computational-basis\n", + "sampling, including when a circuit has no explicit measurement gates. YAQS\n", + "accepts the terminal measurements above; it does not support measurements\n", + "followed by further gates on the measured qubits or classical feedback.\n", + "\n", + "## 2. Set the sampling budget and damping\n", + "\n", + "Each shot produces one bitstring. We use 256 shots per run to keep the example\n", + "quick, with the `fast` preset controlling numerical tolerances." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import DigitalSimParams, NoiseModel\n", + "\n", + "params = DigitalSimParams(shots=256, preset=\"fast\", random_seed=7)\n", + "damping_rate = 1.5\n", + "noise = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": damping_rate}\n", + " for site in range(num_qubits)\n", + "])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "The `lowering` channel relaxes $|1\\rangle$ toward $|0\\rangle$. In circuit\n", + "simulation, `strength` is a Lindblad rate per unit of gate noise time, rather\n", + "than a direct error probability. YAQS applies one unit of noise time after each\n", + "gate on two or more qubits, using only noise processes supported entirely on\n", + "that gate's qubits. Single-qubit gates and idle sites receive no noise.\n", + "\n", + "Here, each end qubit participates in one CZ gate, while each interior qubit\n", + "participates in two. The noise therefore acts during entangling operations,\n", + "rather than as a separate readout-error channel. Simulate a transpiled circuit\n", + "when its compiled gates should determine the noise opportunities.\n", + "\n", + "`shots` sets the total sample budget and must be supplied explicitly. In a noisy\n", + "shots-only run, YAQS uses one stochastic trajectory per shot. The noiseless run\n", + "evolves once and samples that final state repeatedly. Setting `num_traj` does\n", + "not change a shots-only budget. See {doc}`simulation_parameters` for presets and\n", + "the separate roles of shots and trajectories.\n", + "\n", + "## 3. Run the noiseless and noisy cases\n", + "\n", + "Use the same circuit, state, and parameters for both runs so that the comparison\n", + "isolates the added noise. Initialize the simulator separately, then omit the\n", + "noise model for the baseline." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import Simulator\n", + "\n", + "simulator = Simulator(show_progress=False)\n", + "ideal = simulator.run(state, circuit, params)\n", + "damped = simulator.run(state, circuit, params, noise)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "Parallel execution remains enabled by default. `show_progress=False` suppresses\n", + "bars in the documentation; omit it to see progress. The seed repeats the random\n", + "streams for jumps and sampled disorder for the same configuration, but does not\n", + "seed final readout sampling. Shot counts can therefore vary between runs.\n", + "\n", + "## 4. Read individual outcomes\n", + "\n", + "`Result.counts` maps integer outcomes to their counts. Site 0 is the\n", + "least-significant bit, so outcome 1 means that only site 0 is excited.\n", + "Formatting an outcome as binary places site 0 on the right." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0000000000000000: 137 shots, estimated probability 0.535\n", + "0000000000000001: 20 shots, estimated probability 0.078\n", + "1000000000000000: 19 shots, estimated probability 0.074\n", + "0000000000010000: 8 shots, estimated probability 0.031\n", + "0000010000000000: 7 shots, estimated probability 0.027\n" + ] + } + ], + "source": [ + "most_common = sorted(damped.counts.items(), key=lambda item: item[1], reverse=True)[:5]\n", + "for outcome, count in most_common:\n", + " bitstring = format(outcome, f\"0{num_qubits}b\")\n", + " print(f\"{bitstring}: {count} shots, estimated probability {count / params.shots:.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "The counts sum to 256. An outcome absent from the dictionary was not observed;\n", + "its underlying probability need not be zero. To read a specific qubit, use\n", + "`(outcome >> site) & 1`. Bitstring counts preserve spatial information that the\n", + "grouped histogram below discards.\n", + "\n", + "## 5. Compare damping strengths\n", + "\n", + "The quickstart compares a noiseless run with one damped run. Here we add two\n", + "rates to show how the distribution moves as damping increases, reusing the\n", + "baseline and strong-noise calculations above." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "rates = [0.0, 0.1, 0.5, damping_rate]\n", + "results = {0.0: ideal, damping_rate: damped}\n", + "for rate in rates[1:-1]:\n", + " rate_noise = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": rate}\n", + " for site in range(num_qubits)\n", + " ])\n", + " results[rate] = simulator.run(state, circuit, params, rate_noise)\n", + "\n", + "probabilities = np.zeros((len(rates), num_qubits + 1))\n", + "for row, rate in enumerate(rates):\n", + " for outcome, count in results[rate].counts.items():\n", + " probabilities[row, outcome.bit_count()] += count / params.shots" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "`outcome.bit_count()` gives the number of excited qubits, often called the\n", + "Hamming weight. Each row of `probabilities` has 17 bins, from zero to 16\n", + "excitations, and sums to one. Every outcome contributes, including the tails of\n", + "the distribution.\n", + "\n", + "The following histograms use shared axes. The gray outline repeats the sampled\n", + "noiseless baseline in the noisy panels so that the shift remains easy to\n", + "compare." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:36:44.061617\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIXGeneral\"],\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 11,\n", + " \"axes.labelsize\": 11,\n", + " \"axes.linewidth\": 0.7,\n", + " \"xtick.labelsize\": 10,\n", + " \"ytick.labelsize\": 10,\n", + " \"xtick.direction\": \"in\",\n", + " \"ytick.direction\": \"in\",\n", + " \"xtick.top\": True,\n", + " \"ytick.right\": True,\n", + " \"legend.fontsize\": 9,\n", + " \"legend.frameon\": False,\n", + " \"figure.constrained_layout.use\": True,\n", + " \"savefig.dpi\": 180,\n", + "})\n", + "excitation_number = np.arange(num_qubits + 1)\n", + "colors = [\"#0072B2\", \"#009E73\", \"#D55E00\", \"#CC79A7\"]\n", + "fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.6), sharex=True, sharey=True)\n", + "for ax, rate, probability, color, panel in zip(\n", + " axes.flat, rates, probabilities, colors, \"abcd\", strict=True,\n", + "):\n", + " if rate != 0:\n", + " ax.bar(excitation_number, probabilities[0], width=0.85,\n", + " facecolor=\"none\", edgecolor=\"0.55\", linewidth=0.9, label=\"Noiseless\")\n", + " ax.bar(excitation_number, probability, width=0.65, color=color,\n", + " edgecolor=\"white\", linewidth=0.4, alpha=0.9,\n", + " label=\"Noiseless\" if rate == 0 else \"Damped\")\n", + " label = \"Noiseless\" if rate == 0 else rf\"$\\gamma={rate:g}$\"\n", + " ax.set_title(f\"({panel}) {label}\", loc=\"left\", fontsize=11)\n", + " ax.set(xlim=(-0.7, num_qubits + 0.7), xticks=np.arange(0, num_qubits + 1, 4))\n", + " ax.legend(loc=\"upper right\")\n", + "for ax in axes[-1]:\n", + " ax.set_xlabel(\"Number of excited qubits\")\n", + "for ax in axes[:, 0]:\n", + " ax.set_ylabel(\"Measured probability\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "**Readout shifts toward fewer excitations as damping increases.** Each panel\n", + "contains 256 shots from the same 16-qubit circuit. The noiseless distribution is\n", + "centered near eight excitations, while strong damping concentrates probability\n", + "near zero. The baseline outlines use the same samples in all panels.\n", + "\n", + "Without noise, each qubit has excitation probability $1/2$, so the excitation\n", + "number follows a binomial distribution. The graph state's phases do not appear\n", + "in this measurement basis. A matching histogram alone therefore cannot verify\n", + "that the intended entangled state was prepared.\n", + "\n", + "With noise, the shift measures excitation loss during the CZ gates. Different\n", + "bitstrings can have the same excitation number, so use `counts` or site-resolved\n", + "observables when the spatial distribution matters. For a bin of probability $p$,\n", + "independent shots have sampling uncertainty of order\n", + "$\\sqrt{p(1-p)/\\mathtt{shots}}$. Increase `shots` to reduce these fluctuations;\n", + "use tighter presets separately to check numerical error.\n", + "\n", + "## Other measurement workflows\n", + "\n", + "To obtain expectation values instead of counts, supply `observables` on\n", + "`DigitalSimParams`; see {doc}`circuit_observables`. You can request both outputs\n", + "in one call. In a noisy combined run, `num_traj` sets the observable ensemble,\n", + "and YAQS distributes the total `shots` across those trajectories. Multiple shots\n", + "from one trajectory share its noise history, so their uncertainty differs from\n", + "independent one-shot trajectories.\n", + "\n", + "You can pass an OpenQASM source string or file path to `Simulator.run` in place\n", + "of a Qiskit circuit. OpenQASM 3 requires the `qasm3` extra. See\n", + "{doc}`circuit_observables` for an executable example, mid-circuit observable\n", + "checkpoints, and gate-application modes.\n", + "\n", + "## Related topics\n", + "\n", + "- {doc}`simulation_parameters` — sampling budgets and accuracy presets\n", + "- {doc}`realistic_noise_models` — other channels, custom operators, and disorder\n", + "- {ref}`circuit-custom-gates` — custom unitaries and gate translation\n", + "- {doc}`equivalence_checking` — compare circuit behavior" + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 180, + "number_source_lines": true + }, + "source_map": [ + 10, + 32, + 45, + 57, + 66, + 91, + 97, + 110, + 115, + 128, + 144, + 155, + 200 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/digital_analog_simulation.ipynb b/docs/_outputs/examples/digital_analog_simulation.ipynb new file mode 100644 index 000000000..dd208d2b1 --- /dev/null +++ b/docs/_outputs/examples/digital_analog_simulation.ipynb @@ -0,0 +1,11741 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Analog-Digital Simulation\n", + "\n", + "An excitation spreads through the XY chain in {doc}`analog_simulation`. Can\n", + "digital gates bring it back? Here we interrupt that same continuous Hamiltonian\n", + "evolution with a pattern of phase gates. The excitation refocuses at its\n", + "starting site, giving a direct way to see how relaxation and dephasing affect\n", + "the return.\n", + "\n", + "A `SimulationProgram` combines circuits and analog intervals in their execution\n", + "order. YAQS carries the evolving state through the whole program, including each\n", + "noisy trajectory. This guide uses the standard installation and Matplotlib for\n", + "plotting. Run the cells in order in a notebook; for a script, use the\n", + "entry-point guard in {doc}`simulator_initialization`.\n", + "\n", + "## 1. Prepare the chain with a circuit\n", + "\n", + "Use the same 20-site open XY chain and hopping amplitude as the analog and\n", + "{doc}`circuit_observables` guides,\n", + "\n", + "$$\n", + "H=-\\frac{1}{2}\\sum_{i=0}^{L-2}(X_iX_{i+1}+Y_iY_{i+1}).\n", + "$$\n", + "\n", + "Time is measured in inverse hopping units, with $\\hbar=1$. Start from an\n", + "all-zero MPS, then use a digital $X$ gate to prepare one excitation at site 10.\n", + "Site numbers match Qiskit's qubit indices." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "from qiskit import QuantumCircuit\n", + "\n", + "from mqt.yaqs import AnalogSimParams, DigitalSimParams, Hamiltonian, Observable, SimulationProgram, Simulator, State\n", + "\n", + "length = 20\n", + "center = length // 2\n", + "state = State(length, initial=\"zeros\")\n", + "hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0)\n", + "observables = [Observable(\"z\", site) for site in range(length)]\n", + "\n", + "preparation = QuantumCircuit(length)\n", + "preparation.x(center)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "Unlike the circuit guide, we will let YAQS evolve the Hamiltonian directly\n", + "between gates. We need no Trotter circuit to represent those intervals.\n", + "\n", + "## 2. Build the refocusing pulse\n", + "\n", + "After half the evolution, apply a $Z$ gate to every even site. The pulse changes\n", + "phases without changing site occupations at that instant. Every XY bond has\n", + "exactly one pulsed endpoint, so the combined pulse $P$ satisfies $PHP=-H$. The\n", + "subsequent evolution therefore unwinds the earlier spreading." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "phase_pulse = QuantumCircuit(length)\n", + "phase_pulse.z(range(0, length, 2))\n", + "\n", + "half_duration = 1.5\n", + "analog_params = AnalogSimParams(elapsed_time=half_duration, dt=0.25, order=2, preset=\"fast\")\n", + "digital_params = DigitalSimParams(sample_layers=True, preset=\"fast\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "The two analog intervals give a final time of $3$, matching the other transport\n", + "guides. `dt=0.25` samples seven times per interval. We use second-order TJM\n", + "evolution for the noisy comparison. `sample_layers=True` also records the\n", + "observables at each circuit's entry and exit.\n", + "\n", + "The phase pulse reverses this XY Hamiltonian because it changes the sign of\n", + "every exchange term. Added terms such as ZZ interactions generally do not\n", + "reverse under the same pulse; another Hamiltonian needs a separate check.\n", + "\n", + "## 3. Assemble and run the programs\n", + "\n", + "Each segment is an `(operator, params)` pair. A circuit selects digital\n", + "simulation, while a `Hamiltonian` selects analog evolution. Set the shared\n", + "observables and random seed on the program; leave those fields unset on the\n", + "segment parameters. We also set the trajectory budget on the program so it\n", + "applies to the complete sequence." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "free_program = SimulationProgram(\n", + " [\n", + " (preparation, digital_params),\n", + " (hamiltonian, analog_params),\n", + " (hamiltonian, analog_params),\n", + " ],\n", + " observables=observables, num_traj=32, random_seed=7,\n", + ")\n", + "echo_program = SimulationProgram(\n", + " [\n", + " (preparation, digital_params),\n", + " (hamiltonian, analog_params),\n", + " (phase_pulse, digital_params),\n", + " (hamiltonian, analog_params),\n", + " (phase_pulse, digital_params),\n", + " ],\n", + " observables=observables, num_traj=32, random_seed=7,\n", + ")\n", + "\n", + "simulator = Simulator(show_progress=False)\n", + "free_result = simulator.run(state, free_program)\n", + "echo_result = simulator.run(state, echo_program)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "The final phase pulse restores the original frame. With $U(\\tau)=\\exp(-iH\\tau)$,\n", + "the echo part obeys $PU(\\tau)PU(\\tau)=I$ in the noiseless limit. This last pulse\n", + "does not change the plotted occupations, but it also restores the phases of a\n", + "general initial state.\n", + "\n", + "YAQS preserves the input `state`, so both programs start from the same all-zero\n", + "state and apply the same preparation. Noiseless programs need only one\n", + "trajectory. Noisy programs average 32 complete trajectories, with parallel\n", + "execution enabled by default. The documentation suppresses progress bars; omit\n", + "`show_progress=False` to see them.\n", + "\n", + "## 4. Read the occupation heatmaps\n", + "\n", + "The outer result contains stitched `times` and `expectation_values`. Digital\n", + "gates are instantaneous on this timeline, so their samples share timestamps with\n", + "analog boundaries. Each entry in `segment_results` also contains the segment's\n", + "type, time offset, and local output.\n", + "\n", + "For heatmaps, collect the analog samples, add each segment's time offset, and\n", + "remove the repeated midpoint. The $Z$ pulse leaves occupation unchanged there,\n", + "so either adjacent analog sample gives the same value. The initial analog sample\n", + "already includes the digital preparation." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Return occupation: free=0.0244, echo=0.9999\n" + ] + } + ], + "source": [ + "def analog_occupations(result):\n", + " \"\"\"Extract analog times and site occupations from a program result.\"\"\"\n", + " segments = [segment for segment in result.segment_results if segment.segment_type == \"analog\"]\n", + " times = np.concatenate([segment.times + segment.time_offset for segment in segments])\n", + " values = np.concatenate([np.asarray(segment.expectation_values) for segment in segments], axis=1)\n", + " keep = np.r_[True, np.diff(times) > 0]\n", + " return times[keep], (1 - values[:, keep]) / 2\n", + "\n", + "\n", + "times, free_occupation = analog_occupations(free_result)\n", + "_, echo_occupation = analog_occupations(echo_result)\n", + "print(f\"Return occupation: free={free_occupation[center, -1]:.4f}, echo={echo_occupation[center, -1]:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "The occupation array has shape `(20, 13)`. Rows follow the observable list, and\n", + "columns follow the extracted times from $0$ to $3$." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:36:48.842221\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3.0\n", + " \n", + " \n", + " \n", + " Time\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 10\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 15\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 19\n", + " \n", + " \n", + " \n", + " Site\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) Free evolution\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3.0\n", + " \n", + " \n", + " \n", + " Time\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b) Digital refocusing\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.25\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.50\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " O\n", + " c\n", + " c\n", + " u\n", + " p\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " ⟨\n", + " ⟩\n", + " n\n", + " i\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib.colors import PowerNorm\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\", \"font.serif\": [\"STIXGeneral\"], \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 10, \"axes.labelsize\": 11, \"axes.linewidth\": 0.8,\n", + " \"xtick.direction\": \"in\", \"ytick.direction\": \"in\", \"svg.fonttype\": \"none\",\n", + " \"legend.frameon\": False,\n", + "})\n", + "occupation_norm = PowerNorm(gamma=0.5, vmin=0, vmax=1)\n", + "fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.8), sharex=True, sharey=True, layout=\"constrained\")\n", + "for ax, occupation, title in zip(\n", + " axes, [free_occupation, echo_occupation], [\"(a) Free evolution\", \"(b) Digital refocusing\"], strict=True,\n", + "):\n", + " image = ax.pcolormesh(times, np.arange(length), occupation, shading=\"auto\", cmap=\"cividis\", norm=occupation_norm)\n", + " ax.set(xlabel=\"Time\", title=title, yticks=[0, 5, 10, 15, 19], xlim=(0, 3))\n", + "axes[0].set_ylabel(\"Site\")\n", + "axes[1].axvline(half_duration, color=\"white\", linestyle=\"--\", linewidth=1)\n", + "fig.colorbar(image, ax=axes, label=r\"Occupation $\\langle n_i\\rangle$\", ticks=[0, 0.25, 0.5, 1], shrink=0.9)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "**The phase pulse brings the spreading excitation back.** Both programs follow\n", + "the same dynamics until $t=1.5$. Free evolution continues to spread, while the\n", + "pulsed program refocuses at site 10 at $t=3$. The white dashed line marks the\n", + "midpoint pulse. Both panels use the same square-root color scale to retain weak\n", + "occupation without changing the normalization.\n", + "\n", + "## 5. Add relaxation and dephasing\n", + "\n", + "A pulse can reverse coherent spreading, but it cannot reverse an irreversible\n", + "noise process. Uniform relaxation removes the excitation. Local dephasing\n", + "preserves total excitation while disrupting the phases needed for refocusing.\n", + "Apply each noise model to the same echo program." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import NoiseModel\n", + "\n", + "relaxation_rate = 0.5\n", + "dephasing_rates = [0.05, 0.2]\n", + "noise_models = {\n", + " \"Relaxation\": NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": relaxation_rate}\n", + " for site in range(length)\n", + " ]),\n", + " **{\n", + " f\"Dephasing {rate:g}\": NoiseModel([\n", + " {\"name\": \"pauli_z\", \"sites\": [site], \"strength\": rate}\n", + " for site in range(length)\n", + " ])\n", + " for rate in dephasing_rates\n", + " },\n", + "}\n", + "echo_results = {\"No noise\": echo_result}\n", + "for label, noise in noise_models.items():\n", + " echo_results[label] = simulator.run(state, echo_program, noise_model=noise)\n", + "\n", + "occupations = {label: analog_occupations(result)[1] for label, result in echo_results.items()}" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "`strength` is a Lindblad rate in inverse time. The jump operators are\n", + "$\\sqrt{\\gamma_-}\\,|0\\rangle\\langle1|_i$ for relaxation and\n", + "$\\sqrt{\\gamma_z}\\,Z_i$ for dephasing. In this convention an isolated qubit's\n", + "off-diagonal density-matrix entries decay at rate $2\\gamma_z$.\n", + "\n", + "The run-level noise model is inherited by all segments. These one-qubit gates\n", + "receive no stochastic circuit noise in YAQS, so noise acts only during the\n", + "analog intervals here. The pulses are ideal and instantaneous. The same noisy\n", + "trajectory and random stream continue across the midpoint pulse.\n", + "\n", + "For a single excitation with uniform relaxation, the exact total population is\n", + "$N(t)=\\exp(-\\gamma_-t)$. Surviving trajectories still refocus, so the exact\n", + "final return occupation has the same value, about $0.223$ at $t=3$. Under\n", + "Pauli-Z dephasing, $N(t)=1$, but the return becomes weaker and occupation\n", + "remains away from the center. This comparison uses different noise channels;\n", + "their numerical rates do not represent equal physical error strengths." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:37:23.232357\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " Time\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 10\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 19\n", + " \n", + " \n", + " \n", + " Site\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) No noise\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " Time\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 10\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 19\n", + " \n", + " \n", + " \n", + " Site\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (\n", + " b\n", + " )\n", + "  \n", + " R\n", + " e\n", + " l\n", + " a\n", + " x\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + " ,\n", + "  \n", + " =\n", + " 0\n", + " .\n", + " 5\n", + " γ\n", + " −\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " Time\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 10\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 19\n", + " \n", + " \n", + " \n", + " Site\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (\n", + " c\n", + " )\n", + "  \n", + " D\n", + " e\n", + " p\n", + " h\n", + " a\n", + " s\n", + " i\n", + " n\n", + " g\n", + " ,\n", + "  \n", + " =\n", + " 0\n", + " .\n", + " 0\n", + " 5\n", + " γ\n", + " z\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " Time\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 10\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 19\n", + " \n", + " \n", + " \n", + " Site\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (\n", + " d\n", + " )\n", + "  \n", + " D\n", + " e\n", + " p\n", + " h\n", + " a\n", + " s\n", + " i\n", + " n\n", + " g\n", + " ,\n", + "  \n", + " =\n", + " 0\n", + " .\n", + " 2\n", + " γ\n", + " z\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " Time\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.25\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.50\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.75\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " ⟨\n", + " ⟩\n", + " n\n", + " 1\n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (e) Return to the center\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " Time\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " N\n", + " n\n", + " =\n", + " ∑\n", + " ⟨\n", + " ⟩\n", + " i\n", + " i\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (f) Surviving excitation\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " No noise\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Relaxation\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Dephasing 0.05\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Dephasing 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " e\n", + " −\n", + " γ\n", + " t\n", + " −\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.25\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.50\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " O\n", + " c\n", + " c\n", + " u\n", + " p\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " ⟨\n", + " ⟩\n", + " n\n", + " i\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "labels = list(echo_results)\n", + "colors = [\"0.15\", \"#D55E00\", \"#56B4E9\", \"#0072B2\"]\n", + "fig, axes = plt.subplots(3, 2, figsize=(7.2, 6.8), layout=\"constrained\")\n", + "titles = [\"(a) No noise\", r\"(b) Relaxation, $\\gamma_-=0.5$\",\n", + " r\"(c) Dephasing, $\\gamma_z=0.05$\", r\"(d) Dephasing, $\\gamma_z=0.2$\"]\n", + "for ax, label, title in zip(axes[:2].flat, labels, titles, strict=True):\n", + " image = ax.pcolormesh(times, np.arange(length), occupations[label], shading=\"auto\", cmap=\"cividis\", norm=occupation_norm)\n", + " ax.axvline(half_duration, color=\"white\", linestyle=\"--\", linewidth=1)\n", + " ax.set(xlabel=\"Time\", ylabel=\"Site\", title=title, yticks=[0, 10, 19], xlim=(0, 3))\n", + "fig.colorbar(image, ax=list(axes[:2].flat), label=r\"Occupation $\\langle n_i\\rangle$\",\n", + " ticks=[0, 0.25, 0.5, 1], shrink=0.8)\n", + "\n", + "for label, color in zip(labels, colors, strict=True):\n", + " occupation = occupations[label]\n", + " result = echo_results[label]\n", + " segments = [segment for segment in result.segment_results if segment.segment_type == \"analog\"]\n", + " raw_times = np.concatenate([segment.times + segment.time_offset for segment in segments])\n", + " keep = np.r_[True, np.diff(raw_times) > 0]\n", + " trajectories = (1 - np.concatenate([np.asarray(segment.trajectories) for segment in segments], axis=-1)) / 2\n", + " trajectories = trajectories[..., keep]\n", + " for ax, mean, samples in (\n", + " (axes[2, 0], occupation[center], trajectories[center]),\n", + " (axes[2, 1], occupation.sum(axis=0), trajectories.sum(axis=0)),\n", + " ):\n", + " ax.plot(times, mean, color=color, linewidth=1.7, label=label)\n", + " if samples.shape[0] > 1:\n", + " standard_error = samples.std(axis=0, ddof=1) / np.sqrt(samples.shape[0])\n", + " ax.fill_between(times, mean - standard_error, mean + standard_error, color=color, alpha=0.15)\n", + "axes[2, 1].plot(times, np.exp(-relaxation_rate * times), \":\", color=\"#D55E00\", linewidth=1.4, label=r\"$e^{-\\gamma_-t}$\")\n", + "for ax in axes[2]:\n", + " ax.axvline(half_duration, color=\"0.6\", linestyle=\"--\", linewidth=0.8)\n", + " ax.set(xlabel=\"Time\", xlim=(0, 3))\n", + "axes[2, 0].set(title=\"(e) Return to the center\", ylabel=r\"$\\langle n_{10}\\rangle$\")\n", + "axes[2, 1].set(title=\"(f) Surviving excitation\", ylabel=r\"$N=\\sum_i\\langle n_i\\rangle$\")\n", + "axes[2, 1].legend(fontsize=7, loc=\"lower left\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "**Loss and dephasing limit the return in different ways.** Relaxation reduces\n", + "the total population, while the dephased excitation survives in a broader\n", + "spatial distribution. Increasing dephasing weakens the echo over the rates\n", + "shown. The lower panels separate the occupation returning to site 10 from the\n", + "total excitation remaining in the chain. Shading shows one standard error\n", + "estimated from complete trajectories; the dotted line gives the exact relaxation\n", + "envelope. These bands describe sampling uncertainty, not timestep or MPS\n", + "truncation error. Increase `num_traj` to reduce sampling fluctuations, and check\n", + "numerical convergence before interpreting small differences.\n", + "\n", + "## Further options\n", + "\n", + "Programs require an MPS initial state. Segment parameters control local timing,\n", + "accuracy, gate mode, and digital `shots`. Observables, `random_seed`, and\n", + "`get_state` belong on the program. A noiseless program can retain its final\n", + "state with `get_state=True`; noisy programs do not return a single final MPS. A\n", + "program-level `num_traj` overrides segment budgets. If omitted, all segment\n", + "budgets must agree. Digital segments can target qubits in a heterogeneous MPS;\n", + "non-qubit sites remain spectators. For analog-only Hamiltonian changes, use\n", + "`Hamiltonian.piecewise` as described in {doc}`hamiltonians`.\n", + "\n", + "An optional third tuple entry overrides noise for one segment, for example\n", + "`(hamiltonian, analog_params, local_noise)`. `None` inherits run-level noise; an\n", + "empty `NoiseModel()` disables it for that segment. Digital operators may also be\n", + "OpenQASM strings or paths. Pass the pair list directly to `Simulator.run` with\n", + "program-wide keywords when a named program is unnecessary. The outer `counts`\n", + "contains the histogram from the last segment that sampled shots; inspect\n", + "`segment_results` for earlier histograms. Program execution does not support\n", + "`multi_time_observables`.\n", + "\n", + "For deterministic scheduled jumps, see {ref}`noise-scheduled-jumps`. Jump times\n", + "use the analog run's local clock and must follow its `dt` grid with `order=1`.\n", + "Consecutive compatible analog segments share that clock; a digital gate starts a\n", + "new analog run. Use a segment noise override to attach a schedule to one\n", + "interval. For device-specific noise strengths and distributions, see\n", + "{doc}`realistic_noise_models`." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 39, + 53, + 65, + 72, + 91, + 114, + 139, + 152, + 157, + 181, + 196, + 219, + 238, + 276 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/digital_twin.ipynb b/docs/_outputs/examples/digital_twin.ipynb new file mode 100644 index 000000000..a0b794a0c --- /dev/null +++ b/docs/_outputs/examples/digital_twin.ipynb @@ -0,0 +1,2206 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Building a Digital Twin\n", + "\n", + "Noise changes how excitations move through a quantum system. Measurements can\n", + "help us estimate that noise and build a model of the observed dynamics. Here we\n", + "learn relaxation and dephasing rates from measurements at the ends of a\n", + "four-spin chain, then use the fitted model to predict transport through its\n", + "unmeasured interior.\n", + "\n", + "This extends {doc}`quickstart` with data preparation, parameter bounds, and\n", + "validation. We generate synthetic observations so the underlying rates are\n", + "known, but pass only the observed traces to the fitter. The example uses the\n", + "standard YAQS installation and Matplotlib for plotting. Run the cells in order\n", + "in a notebook; for a script, use the entry-point guard in\n", + "{doc}`simulator_initialization`.\n", + "\n", + "## 1. Set up excitation transport\n", + "\n", + "The XY Hamiltonian exchanges excitations between neighboring spins,\n", + "\n", + "$$\n", + "H=-\\frac{1}{2}\\sum_{i=0}^{2}(X_iX_{i+1}+Y_iY_{i+1}).\n", + "$$\n", + "\n", + "We start with one excitation at site 0. As in {doc}`analog_simulation`, the\n", + "hopping amplitude is one and $\\hbar=1$. Measuring $Z_i$ gives the occupation\n", + "through $\\langle n_i\\rangle=(1-\\langle Z_i\\rangle)/2$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseCharacterizer, NoiseModel, Observable, Simulator, State\n", + "\n", + "length = 4\n", + "state = State(length, initial=\"basis\", basis_string=\"1000\", representation=\"density_matrix\")\n", + "hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0)\n", + "observables = [Observable(\"z\", site) for site in range(length)]\n", + "params = AnalogSimParams(observables=observables, elapsed_time=8.0, dt=0.1, preset=\"fast\")\n", + "simulator = Simulator(show_progress=False)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "The density-matrix state gives deterministic Lindblad dynamics without\n", + "trajectory sampling error. We record all four sites to validate predictions\n", + "later; only the endpoints will enter the fit. The documentation suppresses\n", + "progress bars with `show_progress=False`; omit this argument to see progress.\n", + "\n", + "## 2. See how noise changes the dynamics\n", + "\n", + "A local relaxation channel at site 3 removes excitations after they reach the\n", + "far end of the chain. A dephasing channel at site 2 leaves the total excitation\n", + "number unchanged by itself, but changes the interference that drives transport.\n", + "The reference rates are $\\gamma_{\\mathrm{loss}}=0.35$ and $\\gamma_\\phi=0.12$." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "def transport_noise(scale):\n", + " \"\"\"Scale the reference relaxation and dephasing rates together.\"\"\"\n", + " return NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [3], \"strength\": 0.35 * scale},\n", + " {\"name\": \"pauli_z\", \"sites\": [2], \"strength\": 0.12 * scale},\n", + " ])\n", + "\n", + "\n", + "noise_scales = [0.0, 0.5, 1.0, 3.0]\n", + "reference_runs = {}\n", + "occupations = {}\n", + "for scale in noise_scales:\n", + " noise = None if scale == 0 else transport_noise(scale)\n", + " result = simulator.run(state, hamiltonian, params, noise)\n", + " reference_runs[scale] = result\n", + " occupations[scale] = (1 - np.asarray(result.expectation_values)) / 2\n", + "\n", + "times = reference_runs[1.0].times" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "`strength` is a Lindblad rate in inverse time. The jump operators are\n", + "$L_{\\mathrm{loss}}=\\sqrt{\\gamma_{\\mathrm{loss}}}\\,|0\\rangle\\langle1|_3$ and\n", + "$L_\\phi=\\sqrt{\\gamma_\\phi}\\,Z_2$. This convention gives the dephasing term\n", + "$\\gamma_\\phi(Z_2\\rho Z_2-\\rho)$. The dimensionless `scale` multiplies both\n", + "rates; it is not a per-gate error probability.\n", + "\n", + "The four heatmaps share their axes and a square-root color normalization, which\n", + "keeps weak occupation visible while retaining the full range from zero to one." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:37:27.458999\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " S\n", + " i\n", + " t\n", + " e\n", + "  \n", + " i\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) No noise\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b) Half the reference rates\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " S\n", + " i\n", + " t\n", + " e\n", + "  \n", + " i\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (c) Reference rates\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (d) Three times the reference rates\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.25\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.50\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " O\n", + " c\n", + " c\n", + " u\n", + " p\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " ⟨\n", + " ⟩\n", + " n\n", + " i\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib.colors import PowerNorm\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIXGeneral\"],\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 10,\n", + " \"axes.labelsize\": 11,\n", + " \"axes.linewidth\": 0.8,\n", + " \"xtick.direction\": \"in\",\n", + " \"ytick.direction\": \"in\",\n", + " \"svg.fonttype\": \"none\",\n", + "})\n", + "occupation_norm = PowerNorm(gamma=0.5, vmin=0, vmax=1)\n", + "fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.4), sharex=True, sharey=True, layout=\"constrained\")\n", + "titles = [\"(a) No noise\", \"(b) Half the reference rates\", \"(c) Reference rates\", \"(d) Three times the reference rates\"]\n", + "for ax, scale, title in zip(axes.flat, noise_scales, titles, strict=True):\n", + " image = ax.pcolormesh(times, np.arange(length), occupations[scale], shading=\"auto\", cmap=\"cividis\", norm=occupation_norm, rasterized=True)\n", + " ax.set(title=title, yticks=np.arange(length), xlim=(0, 8))\n", + "for ax in axes[-1]:\n", + " ax.set_xlabel(r\"Time $t$\")\n", + "for ax in axes[:, 0]:\n", + " ax.set_ylabel(r\"Site $i$\")\n", + "fig.colorbar(image, ax=axes, label=r\"Occupation $\\langle n_i\\rangle$\", ticks=[0, 0.25, 0.5, 1], shrink=0.92)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "**Noise changes transport and weakens the returning excitation.** Without noise,\n", + "the excitation reflects through the chain while its total population stays one.\n", + "Relaxation removes population, and dephasing changes its spatial distribution.\n", + "Both rates change together in this comparison, so the panels show their combined\n", + "effect. We will fit the reference-rate case and use the other panels only to\n", + "illustrate the physical changes.\n", + "\n", + "## 3. Select the observations and candidate channels\n", + "\n", + "Assume that only the endpoints can be measured. Select rows 0 and 3 of the\n", + "synthetic $Z$ traces, keeping their time samples unchanged." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Observation shape: (2, 81)\n" + ] + } + ], + "source": [ + "fitting_sites = [0, 3]\n", + "fitting_observables = [observables[site] for site in fitting_sites]\n", + "reference_z = np.asarray(reference_runs[1.0].expectation_values)\n", + "measured_z = reference_z[fitting_sites]\n", + "\n", + "print(\"Observation shape:\", measured_z.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "`ref_expectations` must have shape `(n_observables, n_times)`. Here it is\n", + "`(2, 81)`: rows follow `fitting_observables`, and columns follow `params.times`,\n", + "including $t=0$. We fit $Z$ expectations, not occupations; convert measured\n", + "occupations with `measured_z = 1 - 2 * measured_occupations` when necessary.\n", + "Data from another time grid must first be aligned with the simulation grid.\n", + "\n", + "The candidate model specifies which channels exist and where they act. The\n", + "optimizer will change their strengths only. We start both rates at 0.2 and allow\n", + "each to range from zero to one." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": {}, + "outputs": [], + "source": [ + "initial_guess = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [3], \"strength\": 0.2},\n", + " {\"name\": \"pauli_z\", \"sites\": [2], \"strength\": 0.2},\n", + "])\n", + "lower_bounds = np.zeros(2)\n", + "upper_bounds = np.ones(2)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "Bounds and fitted parameters follow the order of `initial_guess.processes`:\n", + "relaxation first, dephasing second. These bounds restrict the search; they are\n", + "not uncertainty intervals. The fit assumes the Hamiltonian, initial state,\n", + "channel types, and channel locations are known. It does not discover an\n", + "arbitrary noise model from the observations.\n", + "\n", + "## 4. Fit the rates\n", + "\n", + "`NoiseCharacterizer` repeatedly simulates candidate models and minimizes the\n", + "mean-squared difference from the supplied traces. Its default backend selection\n", + "uses deterministic Lindblad evolution for this four-spin problem." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Endpoint Z-trace RMSE: 0.0686 → 1.10e-04\n", + "Relaxation rate: 0.3499\n", + "Dephasing rate: 0.1199\n" + ] + } + ], + "source": [ + "characterizer = NoiseCharacterizer(show_progress=False)\n", + "fit = characterizer.characterize(\n", + " hamiltonian,\n", + " params,\n", + " init_state=state,\n", + " init_guess=initial_guess,\n", + " observables=fitting_observables,\n", + " ref_expectations=measured_z,\n", + " x_low=lower_bounds,\n", + " x_up=upper_bounds,\n", + " max_iter=40,\n", + " seed=7,\n", + ")\n", + "\n", + "print(f\"Endpoint Z-trace RMSE: {fit.sqrt_loss_before():.4f} → {fit.trajectory_rmse():.2e}\")\n", + "for name, rate in zip((\"Relaxation\", \"Dephasing\"), fit.best_parameters, strict=True):\n", + " print(f\"{name} rate: {rate:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "With two free parameters, YAQS uses the derivative-free CMA-ES optimizer.\n", + "`max_iter=40` limits its generations, and `seed` fixes the optimizer's random\n", + "search. This seed is separate from `AnalogSimParams.random_seed`, which controls\n", + "stochastic simulation. `NoiseCharacterizer` defaults to in-process execution;\n", + "set `parallel=True` to parallelize trajectories for vector or MPS forward\n", + "models.\n", + "\n", + "For observations $z_{o,t}$, the fitted objective is\n", + "\n", + "$$\n", + "J=\\frac{1}{N_{\\mathrm{obs}}N_t}\\sum_{o,t}\n", + "\\left(z_{o,t}^{\\mathrm{model}}-z_{o,t}^{\\mathrm{data}}\\right)^2.\n", + "$$\n", + "\n", + "`sqrt_loss_before()` reports the initial model's RMSE. `trajectory_rmse()`\n", + "reports the mismatch of the final fitted traces. `best_parameters` gives the\n", + "rates in process order, and `optimal_model` is the fitted `NoiseModel` ready for\n", + "simulation. The known synthetic rates let us check recovery, but a low training\n", + "error alone does not establish unique parameters.\n", + "\n", + "## 5. Predict the unmeasured interior\n", + "\n", + "Rerun the fitted model with all four observables. Sites 1 and 2 were withheld\n", + "from the optimization, so they test predictions beyond the fitted traces." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Withheld interior Z-trace RMSE: 7.10e-05\n" + ] + } + ], + "source": [ + "reconstructed = simulator.run(state, hamiltonian, params, fit.optimal_model)\n", + "fitted_z = np.asarray(reconstructed.expectation_values)\n", + "fitted_occupation = (1 - fitted_z) / 2\n", + "heldout_sites = [1, 2]\n", + "heldout_rmse = np.sqrt(np.mean((fitted_z[heldout_sites] - reference_z[heldout_sites]) ** 2))\n", + "\n", + "print(f\"Withheld interior Z-trace RMSE: {heldout_rmse:.2e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "Compare the full dynamics on the same color scale, then inspect the withheld\n", + "sites as time traces. Reference markers are spaced out for readability; all 81\n", + "samples enter the error calculation." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cell-15", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:37:35.630924\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " S\n", + " i\n", + " t\n", + " e\n", + "  \n", + " i\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) Synthetic reference\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " S\n", + " i\n", + " t\n", + " e\n", + "  \n", + " i\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b) Fitted noise model\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " O\n", + " c\n", + " c\n", + " u\n", + " p\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " ⟨\n", + " ⟩\n", + " n\n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (c) Withheld site 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Fitted prediction\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Withheld reference\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " O\n", + " c\n", + " c\n", + " u\n", + " p\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " ⟨\n", + " ⟩\n", + " n\n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (d) Withheld site 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " O\n", + " c\n", + " c\n", + " u\n", + " p\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " ⟨\n", + " ⟩\n", + " n\n", + " i\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(2, 2, figsize=(7.2, 4.6), layout=\"constrained\")\n", + "for ax, dynamics, title in zip(\n", + " axes[0],\n", + " (occupations[1.0], fitted_occupation),\n", + " (\"(a) Synthetic reference\", \"(b) Fitted noise model\"),\n", + " strict=True,\n", + "):\n", + " image = ax.pcolormesh(times, np.arange(length), dynamics, shading=\"auto\", cmap=\"cividis\", norm=occupation_norm, rasterized=True)\n", + " ax.set(title=title, xlabel=r\"Time $t$\", ylabel=r\"Site $i$\", yticks=np.arange(length), xlim=(0, 8))\n", + "fig.colorbar(image, ax=list(axes[0]), label=r\"Occupation $\\langle n_i\\rangle$\", ticks=[0, 0.5, 1])\n", + "for ax, site, label in zip(axes[1], heldout_sites, (\"(c)\", \"(d)\"), strict=True):\n", + " ax.plot(times, fitted_occupation[site], color=\"#225c80\", lw=1.8, label=\"Fitted prediction\")\n", + " ax.plot(times[::4], occupations[1.0][site, ::4], \"o\", color=\"#bb563b\", ms=3.5, markerfacecolor=\"white\", label=\"Withheld reference\")\n", + " ax.set(title=f\"{label} Withheld site {site}\", xlabel=r\"Time $t$\", ylabel=rf\"Occupation $\\langle n_{site}\\rangle$\", xlim=(0, 8), ylim=(0, 1))\n", + " ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + "axes[1, 0].legend(frameon=False, fontsize=9, loc=\"upper right\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-16", + "metadata": {}, + "source": [ + "**Endpoint observations recover the transport through the interior in this\n", + "model.** The fitted heatmap reproduces the reference, including both withheld\n", + "sites. This supports the fitted model for the specified Hamiltonian, initial\n", + "state, and observation window. It does not certify the assumed channels or\n", + "establish accuracy for other preparations, controls, or longer times. In\n", + "experimental work, reserve independent measurements for this validation step.\n", + "\n", + "## Using measured data\n", + "\n", + "Replace `measured_z` with your measured expectation array and keep the same\n", + "observable and time ordering. Supply exactly one of `ref_expectations` and\n", + "`reference_model`. The latter generates reference traces internally and is a\n", + "shortcut for synthetic benchmarks; it is not needed when measurements are\n", + "already available.\n", + "\n", + "The example contains no measurement noise. Finite-shot data add uncertainty, and\n", + "calibration drift or an incorrect Hamiltonian can also affect the fit. The\n", + "current objective weights every observable and time sample equally; it does not\n", + "accept per-sample uncertainty weights or return confidence intervals for the\n", + "rates. Check residuals against measurement uncertainty and test withheld data\n", + "before interpreting small differences between fitted parameters.\n", + "\n", + "Sparse observations can leave several rate combinations indistinguishable. Use\n", + "more times, observables, or preparations to test identifiability. A successful\n", + "optimization shows that a candidate model fits the chosen data; it does not\n", + "prove that this model is unique or that the environment has no memory. For\n", + "memory-sensitive probing, see {doc}`characterization`.\n", + "\n", + "## Further options\n", + "\n", + "### Forward models and sampling\n", + "\n", + "`NoiseCharacterizer(representation=\"auto\")` uses density matrices up to eight\n", + "qubits, vectors up to ten, and MPS above that size by default. Choose\n", + "`\"density_matrix\"`, `\"vector\"`, or `\"mps\"` explicitly when needed. See\n", + "{doc}`representation_comparison` for the numerical trade-offs.\n", + "\n", + "Vector and MPS fits use trajectory-averaged MCWF and TJM simulations.\n", + "`sim_params.num_traj` controls their sampling budget; increasing it reduces\n", + "sampling error at greater cost. Refine the time step and numerical tolerances as\n", + "well as the trajectory count. A fixed `random_seed` makes the forward runs\n", + "repeatable but does not remove their sampling error. Recheck the fitted model\n", + "with more trajectories and independent seeds before drawing conclusions. For a\n", + "small system, deterministic fitting can also use stochastic or measured\n", + "reference data without making the candidate simulations stochastic.\n", + "\n", + "### Optimizer controls and results\n", + "\n", + "`sigma0` sets the initial CMA-ES search scale, and `popsize` sets the number of\n", + "candidates per generation. A larger search budget or several starting points can\n", + "help assess sensitivity to initialization. When there is only one free parameter\n", + "with finite bounds, YAQS uses a bounded scalar search; `max_iter` then limits\n", + "search evaluations rather than CMA-ES generations. Initial-model and final\n", + "fitted-trajectory evaluations are outside either limit.\n", + "\n", + "`fit.ref_traj`, `fit.fit_traj`, and `fit.times` retain the fitted-observable\n", + "comparison. `fit.loss_history` stores candidate losses; it excludes the initial\n", + "model's separately evaluated baseline. `fit.best_loss` is the best search\n", + "objective, while `fit.sqrt_loss_after()` gives its square root. On stochastic\n", + "backends, the final rerun can differ from the best sampled objective. Inspect\n", + "the traces as well as the optimizer's reported loss.\n", + "\n", + "See {class}`~mqt.yaqs.NoiseCharacterizer` for the full interface and\n", + "{doc}`realistic_noise_models` for supported one-site and two-site jump\n", + "processes." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 39, + 50, + 64, + 83, + 94, + 124, + 138, + 145, + 157, + 164, + 178, + 196, + 223, + 231, + 237, + 256 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/ensemble_evolution.ipynb b/docs/_outputs/examples/ensemble_evolution.ipynb new file mode 100644 index 000000000..bb6d3e3e3 --- /dev/null +++ b/docs/_outputs/examples/ensemble_evolution.ipynb @@ -0,0 +1,1737 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Ensemble Evolution\n", + "\n", + "A two-time correlation follows how a measurement at a later time relates to an\n", + "operator applied to the initial state. Averaging these correlations over several\n", + "initial states helps study spin dynamics and transport. This guide starts with\n", + "one state, compares it with a small ensemble, and then follows local\n", + "spin-current correlations in a periodic chain. All evolution is unitary.\n", + "\n", + "This guide uses the standard YAQS installation and Matplotlib. Run the cells in\n", + "order in a notebook. For a script, use the entry-point guard in\n", + "{doc}`simulator_initialization`.\n", + "\n", + "## 1. Follow one state in an open spin chain\n", + "\n", + "Use six spin-$1/2$ sites with nearest-neighbor XXZ interactions and a transverse\n", + "field. With $S^\\alpha=\\sigma^\\alpha/2$, the Hamiltonian is\n", + "\n", + "```{math}\n", + "H = \\sum_{r=0}^{L-2}\\left[\n", + "J_{xx}(S_r^x S_{r+1}^x+S_r^y S_{r+1}^y)\n", + "+\\Delta S_r^z S_{r+1}^z\\right]\n", + "+h_x\\sum_{r=0}^{L-1}S_r^x.\n", + "```\n", + "\n", + "`Hamiltonian.pauli` uses Pauli matrices, so two-spin coefficients include a\n", + "factor of $1/4$ and the field coefficient includes $1/2$. Set $J_{xx}=1$ and\n", + "$\\hbar=1$, so time is in units of $1/J_{xx}$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State\n", + "\n", + "L = 6\n", + "Jxx = 1.0\n", + "delta = 0.7\n", + "h_x = 0.4\n", + "H_open = Hamiltonian.pauli(\n", + " length=L,\n", + " two_body=[(0.25 * Jxx, \"X\", \"X\"), (0.25 * Jxx, \"Y\", \"Y\"), (0.25 * delta, \"Z\", \"Z\")],\n", + " one_body=[(0.5 * h_x, \"X\")],\n", + " bc=\"open\",\n", + ")\n", + "mid = L // 2\n", + "psi0 = State(L, initial=\"haar-random\", pad=2)\n", + "sim = Simulator(show_progress=False)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "The `haar-random` preset builds a random MPS from Haar-random isometries. Here,\n", + "`pad=2` limits its initial bond dimension to two. This is not a uniformly\n", + "sampled vector from the full Hilbert space. State initialization is unseeded, so\n", + "rerunning the notebook changes the numerical curves. Save the initial MPS\n", + "tensors when you need to reproduce a particular ensemble.\n", + "\n", + "First measure the central site's Pauli $Z$ expectation. `Observable(\"z\", mid)`\n", + "represents $\\sigma^z_m$; divide its expectation by two for $S^z_m$." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "primer_params = AnalogSimParams(\n", + " observables=[Observable(\"z\", mid)],\n", + " elapsed_time=5.1,\n", + " dt=0.15,\n", + " max_bond_dim=64,\n", + " svd_threshold=1e-10,\n", + ")\n", + "result_primer = sim.run(psi0, H_open, primer_params)\n", + "times_primer = result_primer.times\n", + "zexp_primer = result_primer.expectation_values[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-4", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:37:39.680734\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " ⟨\n", + " (\n", + " )\n", + " ⟩\n", + " σ\n", + " t\n", + " m\n", + " z\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Local spin dynamics\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\", \"font.serif\": [\"STIXGeneral\"], \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 10, \"axes.labelsize\": 11, \"axes.linewidth\": 0.8,\n", + " \"xtick.direction\": \"in\", \"ytick.direction\": \"in\", \"svg.fonttype\": \"none\",\n", + " \"legend.frameon\": False,\n", + "})\n", + "fig, ax = plt.subplots(figsize=(5.4, 2.8), layout=\"constrained\")\n", + "ax.plot(times_primer, zexp_primer, color=\"#0072B2\", linewidth=1.8)\n", + "ax.axhline(0, color=\"0.7\", linewidth=0.6)\n", + "ax.set(xlabel=r\"Time $t$\", ylabel=r\"$\\langle\\sigma^z_m(t)\\rangle$\", xlim=(0, 5.1))\n", + "ax.set_title(\"Local spin dynamics\", loc=\"left\", fontsize=11)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-5", + "metadata": {}, + "source": [ + "The local expectation changes even though the whole chain evolves unitarily. A\n", + "single curve depends on its initial state. Two-time correlations let us ask how\n", + "a specified initial perturbation affects the later dynamics.\n", + "\n", + "## 2. Request two-time correlations\n", + "\n", + "For a state $|\\psi_0\\rangle$ and propagator $U(t)$, define\n", + "\n", + "```{math}\n", + "C_{AB}(t)=\\langle\\psi_0|U^\\dagger(t)\\,A\\,U(t)\\,B|\\psi_0\\rangle.\n", + "```\n", + "\n", + "Pass `(A, B)` in `multi_time_observables`: `B` acts at time zero and `A` is\n", + "measured at time $t$. Setting `A` and `B` equal gives an autocorrelation. The\n", + "product need not be Hermitian, so the result can be complex.\n", + "\n", + "The correlation backend takes a `list[State]` of MPS inputs. A list with one\n", + "state gives a single-state result. Reuse `psi0` to connect the correlation\n", + "calculation with the local dynamics above." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-6", + "metadata": {}, + "outputs": [], + "source": [ + "sz_mid = Observable(\"z\", mid)\n", + "sx_mid = Observable(\"x\", mid)\n", + "single_state_params = AnalogSimParams(\n", + " observables=[],\n", + " elapsed_time=5.1,\n", + " dt=0.15,\n", + " max_bond_dim=64,\n", + " svd_threshold=1e-10,\n", + " multi_time_observables=[(sz_mid, sz_mid), (sz_mid, sx_mid)],\n", + ")\n", + "result_single = sim.run([psi0], H_open, single_state_params)\n", + "t_single = result_single.multi_time_times\n", + "czz_single = result_single.multi_time_results[0]\n", + "czx_single = result_single.multi_time_results[1]" + ] + }, + { + "cell_type": "markdown", + "id": "cell-7", + "metadata": {}, + "source": [ + "`multi_time_results` has shape `(2, 35)`: one row per pair, in the supplied\n", + "order, and one column per sampled time. `multi_time_times` supplies the matching\n", + "time axis. The rows contain $C_{zz}$ and $C_{zx}$ for Pauli operators; divide by\n", + "four for spin-$1/2$ correlations. In particular, $C_{zz}(0)=1$.\n", + "\n", + "## 3. Average over initial states\n", + "\n", + "Pass several states to average the same correlations with equal weights. Keep\n", + "the original state as the first member and add three independently initialized\n", + "random MPS. Each state evolves separately, and the list length determines the\n", + "ensemble size; `num_traj` does not set it." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-8", + "metadata": {}, + "outputs": [], + "source": [ + "num_states = 4\n", + "ensemble_states = [psi0, *[State(L, initial=\"haar-random\", pad=2) for _ in range(num_states - 1)]]\n", + "ensemble_params = AnalogSimParams(\n", + " observables=[],\n", + " elapsed_time=5.1,\n", + " dt=0.15,\n", + " max_bond_dim=64,\n", + " svd_threshold=1e-10,\n", + " multi_time_observables=[(sz_mid, sz_mid), (sz_mid, sx_mid)],\n", + ")\n", + "result_ens = sim.run(ensemble_states, H_open, ensemble_params)\n", + "t_ens = result_ens.multi_time_times\n", + "czz_ens = result_ens.multi_time_results[0]\n", + "czx_ens = result_ens.multi_time_results[1]" + ] + }, + { + "cell_type": "markdown", + "id": "cell-9", + "metadata": {}, + "source": [ + "`multi_time_results` now contains the ensemble mean, with the same pair and time\n", + "axes. Ordinary `observables`, if supplied, also produce ensemble means in\n", + "`expectation_values` and individual member data in `trajectories`. Per-member\n", + "two-time correlations are not exposed in `Result`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-10", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:37:41.661800\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " R\n", + " e\n", + " (\n", + " )\n", + " C\n", + " t\n", + " z\n", + " z\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) Autocorrelation\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " One state\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4-state mean\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " R\n", + " e\n", + " (\n", + " )\n", + " C\n", + " t\n", + " z\n", + " x\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b) Cross correlation\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " One state\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4-state mean\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9), layout=\"constrained\")\n", + "for ax, single, average, label in zip(\n", + " axes, [czz_single, czx_single], [czz_ens, czx_ens], [\"zz\", \"zx\"], strict=True,\n", + "):\n", + " ax.plot(t_single, single.real, color=\"0.55\", linestyle=\"--\", linewidth=1.3, label=\"One state\")\n", + " ax.plot(t_ens, average.real, color=\"#0072B2\", linewidth=1.8, label=f\"{num_states}-state mean\")\n", + " ax.axhline(0, color=\"0.75\", linewidth=0.6)\n", + " ax.set(xlabel=r\"Time $t$\", ylabel=rf\"$\\mathrm{{Re}}\\,C_{{{label}}}(t)$\", xlim=(0, 5.1))\n", + " ax.legend(fontsize=8)\n", + "axes[0].set_title(\"(a) Autocorrelation\", loc=\"left\", fontsize=11)\n", + "axes[1].set_title(\"(b) Cross correlation\", loc=\"left\", fontsize=11)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-11", + "metadata": {}, + "source": [ + "The plots compare the real parts; the result retains both real and imaginary\n", + "components. Averaging changes the state-dependent fluctuations, but four states\n", + "do not establish a converged thermal average. This example demonstrates the\n", + "ensemble workflow. Dynamical quantum typicality uses suitable random-state\n", + "sampling to estimate traces; finite-temperature calculations also need thermal\n", + "weighting or filtering. The small random-MPS ensemble here supplies neither a\n", + "convergence study nor finite-temperature preparation.\n", + "\n", + "## 4. Compare local spin-current correlations\n", + "\n", + "A periodic XXZ chain lets us study how spin currents change with the interaction\n", + "strength. For each directed bond $(r,r+1)$, with site indices wrapped modulo\n", + "$L$, define\n", + "\n", + "```{math}\n", + "j_r=J_{xx}\\left(S_r^x S_{r+1}^y-S_r^y S_{r+1}^x\\right).\n", + "```\n", + "\n", + "Measure each bond's autocorrelation and average over bonds and initial states:\n", + "\n", + "```{math}\n", + "C_{\\mathrm{bond}}(t)=\\frac{1}{L}\\sum_r\\langle j_r(t)j_r(0)\\rangle_{\\mathrm{ensemble}}.\n", + "```\n", + "\n", + "The two-site matrix below follows the listed site order, including the periodic\n", + "bond `(L - 1, 0)`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-12", + "metadata": {}, + "outputs": [], + "source": [ + "def spin_current_bond_matrix(j_coupling):\n", + " x = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex)\n", + " y = np.array([[0.0, -1.0j], [1.0j, 0.0]], dtype=complex)\n", + " return 0.25 * j_coupling * (np.kron(x, y) - np.kron(y, x))\n", + "\n", + "\n", + "Ltr = 6\n", + "deltas = [0.1, 1.5]\n", + "states_transport = [State(Ltr, initial=\"haar-random\", pad=2) for _ in range(2)]\n", + "j_mat = spin_current_bond_matrix(Jxx)\n", + "bond_obs = [Observable(j_mat, sites=[r, (r + 1) % Ltr]) for r in range(Ltr)]\n", + "pairs_jj = [(obs, obs) for obs in bond_obs]\n", + "transport_curves = {}\n", + "transport_results = {}\n", + "for d in deltas:\n", + " h_periodic = Hamiltonian.pauli(\n", + " length=Ltr,\n", + " two_body=[(0.25 * Jxx, \"X\", \"X\"), (0.25 * Jxx, \"Y\", \"Y\"), (0.25 * d, \"Z\", \"Z\")],\n", + " one_body=[],\n", + " bc=\"periodic\",\n", + " )\n", + " transport_params = AnalogSimParams(\n", + " observables=[],\n", + " elapsed_time=3.0,\n", + " dt=0.1,\n", + " max_bond_dim=32,\n", + " svd_threshold=1e-10,\n", + " multi_time_observables=pairs_jj,\n", + " )\n", + " result_transport = sim.run(states_transport, h_periodic, transport_params)\n", + " t_transport = result_transport.multi_time_times\n", + " transport_results[d] = result_transport\n", + " transport_curves[d] = result_transport.multi_time_results.mean(axis=0)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-13", + "metadata": {}, + "source": [ + "Each `multi_time_results` array has shape `(6, 31)`, with one row for each bond.\n", + "The final mean over rows gives $C_{\\mathrm{bond}}$. Reusing the same initial\n", + "states for both interaction strengths keeps the ensemble fixed." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cell-14", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:37:44.657555\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.050\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.025\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.000\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.025\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.050\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.075\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.100\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.125\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " R\n", + " e\n", + " (\n", + " )\n", + " C\n", + " t\n", + " b\n", + " o\n", + " n\n", + " d\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Local spin-current autocorrelation\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Δ\n", + " =\n", + " 0\n", + " .\n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Δ\n", + " =\n", + " 1\n", + " .\n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(5.4, 2.9), layout=\"constrained\")\n", + "for d, color in zip(deltas, [\"#0072B2\", \"#D55E00\"], strict=True):\n", + " ax.plot(t_transport, transport_curves[d].real, color=color, linewidth=1.8, label=rf\"$\\Delta={d}$\")\n", + "ax.axhline(0, color=\"0.75\", linewidth=0.6)\n", + "ax.set(xlabel=r\"Time $t$\", ylabel=r\"$\\mathrm{Re}\\,C_{\\mathrm{bond}}(t)$\", xlim=(0, 3))\n", + "ax.set_title(\"Local spin-current autocorrelation\", loc=\"left\", fontsize=11)\n", + "ax.legend(fontsize=9)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-15", + "metadata": {}, + "source": [ + "The curves show how the bond-current correlation depends on the interaction\n", + "strength over this short window. They are not the full total-current\n", + "correlation. For $J=\\sum_r j_r$, the latter contains all cross-bond terms,\n", + "$C_{JJ}(t)=L^{-1}\\sum_{r,s}\\langle j_r(t)j_s(0)\\rangle$. Computing it requires\n", + "$L^2$ operator pairs instead of the $L$ pairs used here.\n", + "\n", + "These small-chain curves do not determine a diffusion constant or a Drude\n", + "weight. For the connection between typicality and current correlations, see\n", + "[Steinigeweg et al., Phys. Rev. Lett. **112**, 120601 (2014)](https://doi.org/10.1103/PhysRevLett.112.120601).\n", + "The broader transport setting is covered in\n", + "[Bertini et al., Rev. Mod. Phys. **93**, 025003 (2021)](https://doi.org/10.1103/RevModPhys.93.025003).\n", + "\n", + "## Scale the calculation\n", + "\n", + "Parallel execution is enabled by default for ensembles with several members. The\n", + "documentation suppresses progress with `show_progress=False`; omit that setting\n", + "to see progress. Increase the number of initial states to check sampling\n", + "convergence, and check timestep and bond-dimension convergence separately. The\n", + "random state's initial `pad` and the evolution's `max_bond_dim` serve different\n", + "purposes. Longer evolution can require larger bonds as entanglement grows.\n", + "\n", + "The list-of-state path requires MPS inputs and a static Hamiltonian. It returns\n", + "ensemble observables and correlations, rather than a final ensemble state.\n", + "\n", + "## Related guides\n", + "\n", + "- {doc}`analog_simulation` — single-state analog evolution and numerical\n", + " settings.\n", + "- {doc}`state_initialization` — random MPS, custom states, and list inputs.\n", + "- {doc}`simulator_initialization` — parallel workers and script execution." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 600, + "number_source_lines": true + }, + "source_map": [ + 10, + 40, + 58, + 69, + 82, + 100, + 122, + 137, + 151, + 166, + 173, + 187, + 216, + 250, + 256, + 266 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/equivalence_checking.ipynb b/docs/_outputs/examples/equivalence_checking.ipynb new file mode 100644 index 000000000..7725e09fa --- /dev/null +++ b/docs/_outputs/examples/equivalence_checking.ipynb @@ -0,0 +1,1233 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Circuit Verification\n", + "\n", + "A circuit must use the gates and connections that a quantum device supports.\n", + "Transpilation makes those changes, but the compiled circuit should still perform\n", + "the intended operation. We first verify a circuit compiled for a hardware\n", + "target, then introduce a rotation-angle bug and ask how hardware noise changes\n", + "its agreement with the original circuit.\n", + "\n", + "This extends the circuit comparison in {doc}`quickstart`. The four-qubit example\n", + "uses the standard YAQS installation and Matplotlib for plotting, without a\n", + "hardware account. Run the cells in order in a notebook; for a script, use the\n", + "entry-point guard in {doc}`simulator_initialization`.\n", + "\n", + "## 1. Compile for hardware constraints\n", + "\n", + "Our circuit entangles qubit 0 with every other qubit, then applies local\n", + "rotations. A device with a line of nearest-neighbor connections cannot execute\n", + "all three controlled-X gates directly. The transpiler must route the circuit and\n", + "express its rotations in the device's native gate set." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Original gates: {'ry': 4, 'cx': 3, 'h': 1}\n", + "Compiled gates: {'rz': 10, 'sx': 9, 'cx': 9}\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from qiskit import QuantumCircuit, transpile\n", + "from qiskit.providers.fake_provider import GenericBackendV2\n", + "\n", + "num_qubits = 4\n", + "original = QuantumCircuit(num_qubits)\n", + "original.h(0)\n", + "for site in range(1, num_qubits):\n", + " original.cx(0, site)\n", + "for site in range(num_qubits):\n", + " original.ry(0.3 * (site + 1), site)\n", + "\n", + "connections = [[0, 1], [1, 0], [1, 2], [2, 1], [2, 3], [3, 2]]\n", + "backend = GenericBackendV2(\n", + " num_qubits,\n", + " basis_gates=[\"rz\", \"sx\", \"x\", \"cx\"],\n", + " coupling_map=connections,\n", + " noise_info=False,\n", + ")\n", + "compiled = transpile(\n", + " original,\n", + " backend=backend,\n", + " initial_layout=list(range(num_qubits)),\n", + " optimization_level=1,\n", + " seed_transpiler=7,\n", + ")\n", + "\n", + "print(\"Original gates:\", dict(original.count_ops()))\n", + "print(\"Compiled gates:\", dict(compiled.count_ops()))" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "`GenericBackendV2` supplies an **offline hardware target**, not measured device\n", + "data. To compile for a real device, pass that device's Qiskit backend instead.\n", + "The native gates and connectivity then come from its target. YAQS does not\n", + "import the backend's calibration data into a noise model; we define the noise\n", + "separately below.\n", + "\n", + "## 2. Align the outputs and verify the compiled circuit\n", + "\n", + "Routing can leave logical outputs on different physical qubits. The checker\n", + "compares circuit wires directly, so we must account for this mapping before\n", + "interpreting a mismatch as a compiler bug. We fixed the initial placement to\n", + "`[0, 1, 2, 3]`; the remaining change is the final output permutation.\n", + "\n", + "Append that permutation to the **reference** circuit. The compiled circuit stays\n", + "as the device would execute it, including its routing gates. Qiskit's\n", + "`PermutationGate` lists the input wire for each output position, so we invert\n", + "the logical-to-physical map returned by `final_index_layout()`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Logical output → physical qubit: [2, 0, 1, 3]\n", + "Equivalent: True\n", + "Overlap: 1.000000000000\n" + ] + } + ], + "source": [ + "from qiskit.circuit.library import PermutationGate\n", + "\n", + "from mqt.yaqs import EquivalenceChecker\n", + "\n", + "output_mapping = compiled.layout.final_index_layout()\n", + "reference = original.copy()\n", + "reference.append(PermutationGate(np.argsort(output_mapping).tolist()), range(num_qubits))\n", + "reference = reference.decompose(gates_to_decompose=[\"permutation\"])\n", + "\n", + "checker = EquivalenceChecker()\n", + "verified = checker.check(reference, compiled)\n", + "\n", + "print(\"Logical output → physical qubit:\", output_mapping)\n", + "print(\"Equivalent:\", verified[\"equivalent\"])\n", + "print(f\"Overlap: {verified['fidelity']:.12f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "For the reference unitary $U$ and compiled unitary $V$, the returned overlap is\n", + "\n", + "$$\n", + "a=\\frac{|\\operatorname{Tr}(UV^\\dagger)|}{2^n}.\n", + "$$\n", + "\n", + "An overlap of one means that the circuits agree on every input state, up to a\n", + "global phase. `equivalent` tests whether this value reaches the checker's\n", + "`fidelity` setting, which defaults to `1 - 1e-13`. The compiled circuit passes\n", + "this numerical check. This compares the full operation, rather than only the\n", + "output from one chosen input state.\n", + "\n", + "The default checker selects its backend automatically: dense matrices for at\n", + "most seven qubits, and a matrix product operator (MPO) for larger circuits. This\n", + "small example therefore uses the matrix backend.\n", + "\n", + "```{note}\n", + "The mapping above assumes the identity initial placement and equal circuit\n", + "widths. For another initial layout or a backend that adds ancillas, also align\n", + "the input wires and the circuit widths before checking. YAQS does not apply\n", + "Qiskit's transpilation layout automatically.\n", + "```\n", + "\n", + "## 3. Introduce a rotation-angle bug\n", + "\n", + "Suppose a compiler pass changes one native $R_z$ angle by $\\delta$. We copy the\n", + "compiled circuit and change its first `rz` instruction, leaving the routing and\n", + "all other gates intact." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Buggy circuit equivalent: False\n", + "Overlap with a π/2 angle error: 0.7071\n" + ] + } + ], + "source": [ + "def with_angle_error(circuit, delta):\n", + " \"\"\"Offset the first native Z rotation by delta radians.\"\"\"\n", + " changed = circuit.copy()\n", + " index = next(i for i, instruction in enumerate(changed.data) if instruction.operation.name == \"rz\")\n", + " instruction = changed.data[index]\n", + " rotation = instruction.operation.copy()\n", + " rotation.params[0] += delta\n", + " changed.data[index] = instruction.replace(operation=rotation)\n", + " return changed\n", + "\n", + "\n", + "angles = np.linspace(0, np.pi, 25)\n", + "angle_overlaps = [checker.check(reference, with_angle_error(compiled, delta))[\"fidelity\"] for delta in angles]\n", + "bug_angle = np.pi / 2\n", + "buggy = with_angle_error(compiled, bug_angle)\n", + "bug_result = checker.check(reference, buggy)\n", + "\n", + "print(\"Buggy circuit equivalent:\", bug_result[\"equivalent\"])\n", + "print(f\"Overlap with a π/2 angle error: {bug_result['fidelity']:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "For this single rotation error, the overlap is exactly $|\\cos(\\delta/2)|$ in\n", + "exact arithmetic. The other gates cancel inside the trace, so the formula does\n", + "not depend on where the faulty rotation occurs. A $\\pi/2$ error gives an overlap\n", + "of about 0.707 and fails the equivalence check. A smaller error also fails once\n", + "its overlap falls below the chosen tolerance.\n", + "\n", + "(equivalence-noise-model)=\n", + "\n", + "## 4. Add a hardware noise model\n", + "\n", + "A correctly compiled circuit can still deviate from the intended operation\n", + "because its gates are noisy. We model a Pauli error after each controlled-X\n", + "gate: on each participating qubit, apply $X$, $Y$, or $Z$ with probability $p/3$\n", + "each, and apply no error with probability $1-p$. Sweeping $p$ separates the\n", + "noiseless compiler check from the effect of executing the circuit on noisy\n", + "hardware." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import NoiseModel\n", + "\n", + "probabilities = np.array([0.0, 0.005, 0.015, 0.03, 0.06, 0.1])\n", + "implementations = {\"Correct compilation\": compiled, \"Rotation-angle bug\": buggy}\n", + "noise_results = {label: [] for label in implementations}\n", + "\n", + "for probability in probabilities:\n", + " noise = NoiseModel([\n", + " {\"name\": f\"pauli_{axis}\", \"sites\": [site], \"strength\": float(probability / 3)}\n", + " for site in range(num_qubits)\n", + " for axis in \"xyz\"\n", + " ])\n", + " for label, circuit in implementations.items():\n", + " result = checker.check(\n", + " reference,\n", + " circuit,\n", + " noise_model=noise,\n", + " num_traj=256,\n", + " random_seed=7,\n", + " )\n", + " noise_results[label].append(result)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "**Noise acts on the second circuit only.** Every supported two-qubit unitary is\n", + "a noise opportunity when it contains all sites of a process. Thus routing gates\n", + "also contribute noise. Single-qubit gates, barriers, measurements, and gates on\n", + "three or more qubits do not create noise opportunities.\n", + "\n", + "For the checker, `strength` is a\n", + "**dimensionless probability per eligible gate**, not a Lindblad rate or a\n", + "probability for the whole circuit. This differs from noise strengths in\n", + "{doc}`analog_simulation` and {doc}`circuit_observables`. Processes with the same\n", + "exact support are mutually exclusive, and their probabilities must sum to at\n", + "most one. Different supports are sampled independently, including overlapping\n", + "supports.\n", + "\n", + "Each trajectory gives a unitary $V_r$ with sampled Pauli errors. The noisy\n", + "result reports the square root of the estimated process fidelity,\n", + "\n", + "$$\n", + "\\mathtt{fidelity}=\\sqrt{\\frac{1}{N}\\sum_{r=1}^{N}\n", + "\\left|\\frac{\\operatorname{Tr}(UV_r^\\dagger)}{2^n}\\right|^2}.\n", + "$$\n", + "\n", + "This is the root-mean-square trajectory overlap, not the mean overlap or a\n", + "measurement success probability. `fidelity_error` estimates its Monte Carlo\n", + "standard error. Increasing `num_traj` reduces sampling uncertainty; it does not\n", + "make the assumed hardware model more accurate.\n", + "\n", + "## 5. Compare the bug and noise effects\n", + "\n", + "The left panel checks the controlled rotation error against its exact formula.\n", + "The right panel compares correct and faulty compilations under the same noise\n", + "model. Plotting code is folded so the verification workflow remains visible." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:37:58.048394\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " R\n", + " o\n", + " t\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + " -\n", + " a\n", + " n\n", + " g\n", + " l\n", + " e\n", + "  \n", + " e\n", + " r\n", + " r\n", + " o\n", + " r\n", + "  \n", + " /\n", + " δ\n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " Root process fidelity\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a)\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " |\n", + " c\n", + " o\n", + " s\n", + " (\n", + " /\n", + " 2\n", + " )\n", + " |\n", + " δ\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " YAQS\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " B\n", + " u\n", + " g\n", + " :\n", + "  \n", + " =\n", + " /\n", + " 2\n", + " δ\n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 10\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " P\n", + " a\n", + " u\n", + " l\n", + " i\n", + " -\n", + " e\n", + " r\n", + " r\n", + " o\n", + " r\n", + "  \n", + " p\n", + " r\n", + " o\n", + " b\n", + " a\n", + " b\n", + " i\n", + " l\n", + " i\n", + " t\n", + " y\n", + "  \n", + " p\n", + " e\n", + " r\n", + "  \n", + " g\n", + " a\n", + " t\n", + " e\n", + "  \n", + " q\n", + " u\n", + " b\n", + " i\n", + " t\n", + "  \n", + "  \n", + " (\n", + " %\n", + " )\n", + " p\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b)\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Correct compilation\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Rotation-angle bug\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIXGeneral\"],\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 10,\n", + " \"axes.labelsize\": 11,\n", + " \"axes.linewidth\": 0.8,\n", + " \"xtick.direction\": \"in\",\n", + " \"ytick.direction\": \"in\",\n", + " \"xtick.top\": True,\n", + " \"ytick.right\": True,\n", + " \"svg.fonttype\": \"none\",\n", + "})\n", + "%config InlineBackend.figure_formats = ['svg']\n", + "\n", + "colors = [\"#225c80\", \"#bb563b\"]\n", + "fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.8), sharey=True, layout=\"constrained\")\n", + "fine_angles = np.linspace(0, np.pi, 200)\n", + "axes[0].plot(fine_angles / np.pi, np.abs(np.cos(fine_angles / 2)), color=\"0.55\", lw=1.7, label=r\"$|\\cos(\\delta/2)|$\")\n", + "axes[0].plot(angles / np.pi, angle_overlaps, \"o\", ms=3.5, color=colors[0], label=\"YAQS\")\n", + "axes[0].plot(0.5, bug_result[\"fidelity\"], \"D\", ms=5, color=colors[1], label=r\"Bug: $\\delta=\\pi/2$\")\n", + "axes[0].set(xlabel=r\"Rotation-angle error $\\delta/\\pi$\", ylabel=\"Root process fidelity\", xlim=(-0.03, 1.03))\n", + "axes[0].legend(frameon=False, fontsize=9, loc=\"lower left\")\n", + "\n", + "for (label, results), color in zip(noise_results.items(), colors, strict=True):\n", + " values = [result[\"fidelity\"] for result in results]\n", + " errors = [result[\"fidelity_error\"] for result in results]\n", + " axes[1].errorbar(100 * probabilities, values, yerr=errors, color=color, marker=\"o\", ms=4, lw=1.6, capsize=2.5, label=label)\n", + "axes[1].set(xlabel=r\"Pauli-error probability per gate qubit $p$ (%)\", xlim=(-0.3, 10.3))\n", + "axes[1].legend(frameon=False, fontsize=9, loc=\"lower left\")\n", + "for label, ax in zip((\"(a)\", \"(b)\"), axes, strict=True):\n", + " ax.text(0.02, 1.03, label, transform=ax.transAxes, va=\"bottom\", fontweight=\"bold\")\n", + " ax.set_ylim(-0.04, 1.06)\n", + " ax.spines[[\"top\", \"right\"]].set_visible(False)\n", + " ax.tick_params(top=False, right=False)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "**Compiler errors and hardware noise reduce agreement in different ways.** (a)\n", + "The noiseless overlap follows the single-angle error formula. (b) With no noise,\n", + "the correct compilation agrees with the reference, while the faulty circuit\n", + "starts below one. As the Pauli-error probability increases, both estimates fall\n", + "in this example. Error bars show one Monte Carlo standard error from 256\n", + "trajectories. Lines join sampled points.\n", + "\n", + "This comparison tells us whether compilation preserves the intended operation\n", + "and how a specified hardware model changes its process fidelity. It does not\n", + "identify an unknown bug or infer device noise from measurements. For learning\n", + "noise strengths from observed dynamics, see {doc}`digital_twin`.\n", + "\n", + "For noisy results, `equivalent` applies the same overlap threshold to the point\n", + "estimate without using its error bar. Treat this as a sampled comparison, not an\n", + "exact noisy-channel certificate. In particular, a zero reported error bar when\n", + "every sampled overlap is zero does not establish zero uncertainty.\n", + "\n", + "## Further options\n", + "\n", + "### Accuracy and backends\n", + "\n", + "Keep `representation=\"auto\"` for automatic selection, or choose `\"matrix\"` or\n", + "`\"mpo\"` explicitly. Dense matrix storage grows as $4^n$; MPO cost depends on the\n", + "operator's bond dimensions and can also grow rapidly. The MPO method is\n", + "described in {footcite:p}`sander2025_EquivalenceChecking`.\n", + "\n", + "The constructor's `fidelity` sets the decision threshold, while `threshold` sets\n", + "the MPO singular-value cutoff. These control different errors. For an\n", + "approximate comparison, choose a decision threshold that matches your purpose\n", + "and check that numerical truncation does not determine the answer. The returned\n", + "`representation` records which backend ran.\n", + "\n", + "Terminal measurements are ignored for unitary checks; mid-circuit measurements\n", + "are unsupported. Decompose gates on more than two qubits before using the MPO\n", + "backend. See {ref}`circuit-custom-gates` for supported gate translation.\n", + "\n", + "### Noise models and returned data\n", + "\n", + "The checker supports stochastic Pauli errors, including Pauli products. It\n", + "rejects dissipative channels such as relaxation; use the simulator for those\n", + "channels. See {doc}`realistic_noise_models` for model construction.\n", + "Distribution-valued strengths are drawn once per `check`, then held fixed across\n", + "its trajectories. The resolved same-support probabilities must still sum to at\n", + "most one.\n", + "\n", + "Pass `return_trajectories=True` to keep each trajectory result. Noisy results\n", + "have `matrix=None` and `mpo=None` because the ensemble is a channel. For MPO\n", + "checks, returned entropies and zero-padded Schmidt spectra are trajectory means,\n", + "not spectra of that channel. `fidelity_error` is `None` for a single trajectory.\n", + "\n", + "### Inputs and execution\n", + "\n", + "`check` accepts Qiskit circuits, OpenQASM file paths, and raw OpenQASM source\n", + "strings. OpenQASM 3 requires the optional `mqt-yaqs[qasm3]` extra. File paths\n", + "allow includes to resolve relative to the source file.\n", + "\n", + "Parallel execution is enabled by default. `max_workers` caps concurrency, and\n", + "`mp_context` controls the start method for noisy process pools. A nonnegative\n", + "`random_seed` makes sampled trajectories reproducible across worker scheduling.\n", + "See {class}`~mqt.yaqs.EquivalenceChecker` for the full settings and returned\n", + "fields.\n", + "\n", + "```{footbibliography}\n", + "```" + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 32, + 62, + 82, + 98, + 129, + 149, + 168, + 190, + 224, + 264 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/hamiltonians.ipynb b/docs/_outputs/examples/hamiltonians.ipynb new file mode 100644 index 000000000..feca6e456 --- /dev/null +++ b/docs/_outputs/examples/hamiltonians.ipynb @@ -0,0 +1,443 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Building Hamiltonians\n", + "\n", + "A Hamiltonian defines the energies and interactions in an analog simulation.\n", + "Build one with a named model, a sum of Pauli terms, or your own operator data,\n", + "then pass it to `Simulator.run`. Match its site count and local dimensions to\n", + "those of the initial `State`.\n", + "\n", + "## Choose a built-in model\n", + "\n", + "Most builders create a matrix product operator (MPO), which stores the operator\n", + "as a tensor network. Use the `Hamiltonian` methods directly when available; wrap\n", + "an MPO with `Hamiltonian.from_mpo` for the other models.\n", + "\n", + "| Model | Constructor | Site layout |\n", + "| ------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------- |\n", + "| Transverse-field Ising | {meth}`~mqt.yaqs.Hamiltonian.ising` | Qubits. |\n", + "| Heisenberg or XY | {meth}`~mqt.yaqs.Hamiltonian.heisenberg` | Qubits. |\n", + "| On-site and nearest-neighbor Pauli terms | {meth}`~mqt.yaqs.Hamiltonian.pauli` | Qubits. |\n", + "| Indexed Pauli strings, including long-range terms | {meth}`~mqt.yaqs.MPO.from_pauli_sum` | Qubits; wrap the MPO. |\n", + "| 1D Fermi–Hubbard | {meth}`~mqt.yaqs.Hamiltonian.fermi_hubbard_1d` | Dimension-four sites, or a Jordan–Wigner qubit chain. |\n", + "| Bose–Hubbard | {meth}`~mqt.yaqs.MPO.bose_hubbard` | Truncated boson occupation; wrap the MPO. |\n", + "| Coupled transmons and resonators | {meth}`~mqt.yaqs.Hamiltonian.coupled_transmon` | Alternating transmon and resonator dimensions. |\n", + "| Trapped ion position grid | {meth}`~mqt.yaqs.MPO.trapped_ion` | One grid per ion, for one or two ions; wrap the MPO. |\n", + "\n", + "YAQS evolves with $\\exp(-itH)$, using $\\hbar=1$. Hamiltonian coefficients and\n", + "times must use consistent units. For energies in SI units, divide the operator\n", + "by $\\hbar$ before evolving with time in seconds.\n", + "\n", + "## Build a spin chain\n", + "\n", + "For an open chain, the Ising shortcut constructs\n", + "\n", + "$$\n", + "H_{\\mathrm{Ising}}=-J\\sum_{i=0}^{L-2}Z_iZ_{i+1}-g\\sum_{i=0}^{L-1}X_i.\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import Hamiltonian, State\n", + "\n", + "length = 4\n", + "hamiltonian = Hamiltonian.ising(length, J=1.0, g=0.5)\n", + "state = State(length, initial=\"zeros\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "The Heisenberg shortcut uses the sign convention\n", + "\n", + "$$\n", + "H_{\\mathrm{Heisenberg}}=-\\sum_{i=0}^{L-2}\n", + "\\left(J_xX_iX_{i+1}+J_yY_iY_{i+1}+J_zZ_iZ_{i+1}\\right)\n", + "-h\\sum_{i=0}^{L-1}Z_i.\n", + "$$\n", + "\n", + "Here, $X$, $Y$, and $Z$ are Pauli matrices with eigenvalues $\\pm1$, not spin\n", + "operators with eigenvalues $\\pm1/2$. Setting `Jz=0` gives the XY model used in\n", + "{doc}`analog_simulation`:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "xy = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "These builders and `Hamiltonian.pauli` use open boundaries by default. Set\n", + "`bc=\"periodic\"` to include the bond from the last site to site 0.\n", + "\n", + "## Specify your own Pauli terms\n", + "\n", + "`Hamiltonian.pauli` repeats each `two_body` term over neighboring sites and each\n", + "`one_body` term over all sites. Coefficients enter with the sign you supply. For\n", + "example, the following builds the same XY Hamiltonian as above:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "xy_from_terms = Hamiltonian.pauli(\n", + " length=length,\n", + " two_body=[(-0.5, \"X\", \"X\"), (-0.5, \"Y\", \"Y\")],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "Add an on-site field with an entry such as `one_body=[(-0.2, \"Z\")]`. These\n", + "structured builders require finite real coefficients and qubit sites.\n", + "\n", + "For site-dependent fields, separated sites, or longer strings, build an MPO from\n", + "explicit `(coefficient, string)` pairs:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import MPO\n", + "\n", + "mpo = MPO()\n", + "mpo.from_pauli_sum(\n", + " terms=[(0.4, \"Z0 Z3\"), (0.2, \"Y1\"), (0.1, \"\")],\n", + " length=length,\n", + ")\n", + "custom = Hamiltonian.from_mpo(mpo)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "This operator is $0.4Z_0Z_3+0.2Y_1+0.1I$. Site indices start at zero; omitted\n", + "sites act as identities, and an empty string denotes the identity operator.\n", + "Labels `I`, `X`, `Y`, and `Z` are case-insensitive. Use real coefficients for\n", + "Hermitian Pauli terms.\n", + "\n", + "## Use other local dimensions\n", + "\n", + "Hubbard and device models need an initial state with the same local dimensions\n", + "as the Hamiltonian. For a uniform layout, use `physical_dimensions=local_dim`;\n", + "for different dimensions, supply a list in site order. The\n", + "{doc}`state_initialization` guide explains these preparations.\n", + "\n", + "The coupled-transmon builder alternates transmons and resonators, starting with\n", + "a transmon. Its `length` counts both kinds of sites. The trapped ion builder\n", + "uses one site per ion, with a local dimension equal to the number of grid\n", + "points. See {doc}`transmon_emulation` and {doc}`trapped_ion` for worked device\n", + "examples, noise, and the relevant units.\n", + "\n", + ":::{dropdown} Fermi–Hubbard: physical sites and Jordan–Wigner orbitals\n", + "\n", + "The default builder uses dimension-four sites with local basis $|0\\rangle$,\n", + "$|\\!\\downarrow\\rangle$, $|\\!\\uparrow\\rangle$, $|\\!\\uparrow\\downarrow\\rangle$:\n", + "\n", + "```python\n", + "num_sites = 3\n", + "fermi = Hamiltonian.fermi_hubbard_1d(num_sites, t=1.0, u=0.5)\n", + "fermi_state = State(num_sites, physical_dimensions=4)\n", + "```\n", + "\n", + "This mode uses ladder operators on composite sites. For a Pauli-chain model with\n", + "full Jordan–Wigner signs between spin orbitals, set `jordan_wigner=True`:\n", + "\n", + "```python\n", + "jw = Hamiltonian.fermi_hubbard_1d(2 * num_sites, t=1.0, u=0.5, jordan_wigner=True)\n", + "jw_state = State(2 * num_sites)\n", + "```\n", + "\n", + "The two modes use different hopping-sign conventions. Match both the state and\n", + "operator basis when comparing them.\n", + "\n", + "Here, `length` counts spin orbitals and must be even and at least two. Site\n", + "order is $1\\uparrow,1\\downarrow,2\\uparrow,2\\downarrow,\\ldots$. Both builders use\n", + "open boundaries and omit a chemical-potential term. The\n", + "{func}`~mqt.yaqs.core.libraries.circuit_library.create_1d_fermi_hubbard_circuit`\n", + "provides a digital Trotter circuit with a chemical-potential option.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} Bose–Hubbard and the occupation cutoff\n", + "\n", + "`local_dim` retains occupations from zero through `local_dim - 1` at each site.\n", + "The builder includes an on-site frequency, an interaction $U n_i(n_i-1)/2$, and\n", + "nearest-neighbor hopping with coefficient $-J$:\n", + "\n", + "```python\n", + "local_dim = 3\n", + "bose = Hamiltonian.from_mpo(\n", + " MPO.bose_hubbard(\n", + " length=3,\n", + " local_dim=local_dim,\n", + " omega=1.0,\n", + " hopping_j=0.2,\n", + " hubbard_u=0.5,\n", + " )\n", + ")\n", + "bose_state = State(3, physical_dimensions=local_dim)\n", + "```\n", + "\n", + "Choose a large enough occupation cutoff for your preparation and dynamics. Check\n", + "convergence by increasing `local_dim` when higher occupations matter.\n", + "\n", + ":::\n", + "\n", + "## Supply a dense or sparse matrix\n", + "\n", + "For a small custom operator, pass exactly one of `matrix`, `sparse_matrix`, or\n", + "`tensors`. Dense and sparse matrices must be finite, square, and Hermitian. The\n", + "manual constructor uses a uniform `physical_dimension`, defaulting to two; it\n", + "infers `length` from the matrix size when omitted.\n", + "\n", + "(physical-site-ordering)=\n", + "\n", + "### Physical-site ordering\n", + "\n", + "Dense and sparse matrices use site 0 as the least-significant, fastest-varying\n", + "subsystem, matching Qiskit's qubit ordering. An operator acting on site 0 is\n", + "therefore the rightmost Kronecker factor:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from scipy.sparse import csr_matrix\n", + "\n", + "identity = np.eye(2, dtype=complex)\n", + "pauli_x = np.array([[0, 1], [1, 0]], dtype=complex)\n", + "x_on_site_0 = np.kron(identity, pauli_x)\n", + "\n", + "dense = Hamiltonian(matrix=x_on_site_0)\n", + "sparse = Hamiltonian(sparse_matrix=csr_matrix(x_on_site_0))" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "For local dimensions $d_0,d_1,\\ldots$, the flat index for basis digits\n", + "$(s_0,s_1,\\ldots)$ is $s_0+d_0s_1+d_0d_1s_2+\\cdots$. This order applies to full\n", + "spatial state vectors and operator matrices. Local observable and noise matrices\n", + "use the order of their explicit site list: for example,\n", + "`Observable(np.kron(X, Z), sites=[0, 1])` means $X_0Z_1$. Custom adjacent\n", + "two-site noise matrices require ascending site lists. Circuit matrices follow\n", + "Qiskit's gate convention.\n", + "\n", + "The initial state's representation selects the analog backend, independently of\n", + "the Hamiltonian's source data. See {doc}`representation_comparison` for the\n", + "supported choices.\n", + "\n", + "```{warning}\n", + "Converting a sparse matrix to an MPO densifies the full operator. A sparse\n", + "source therefore does not avoid the full matrix allocation when you run with an\n", + "MPS state. For large MPS simulations, build an MPO directly with a preset, Pauli\n", + "terms, or custom tensors.\n", + "```\n", + "\n", + "## Time-dependent Hamiltonians\n", + "\n", + "Use `Hamiltonian.piecewise` for an analog quench: evolve under one static\n", + "Hamiltonian and then another, on a shared time grid. Every duration must be a\n", + "positive integer multiple of `dt`, and their sum must equal `elapsed_time`. All\n", + "pieces must have matching site counts and local dimensions:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import AnalogSimParams, Observable, Simulator\n", + "\n", + "quench = Hamiltonian.piecewise([\n", + " (hamiltonian, 0.2),\n", + " (Hamiltonian.ising(length, J=1.0, g=2.0), 0.2),\n", + "])\n", + "params = AnalogSimParams(\n", + " observables=[Observable(\"z\", 0)], elapsed_time=quench.duration, dt=0.05\n", + ")\n", + "\n", + "sim = Simulator(show_progress=False)\n", + "result = sim.run(state, quench, params)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "The result contains nine sample times, including time zero, on one continuous\n", + "timeline. This path supports a single MPS state and the default TDVP evolution;\n", + "it also accepts a shared noise model. It does not support dense state\n", + "representations, BUG evolution, or list-of-state ensembles.\n", + "\n", + "For digital gates, different time steps, or segment-specific noise, use a\n", + "`SimulationProgram`; see {doc}`digital_analog_simulation`. A piecewise\n", + "Hamiltonian has no single static MPO or matrix. Select a static entry from\n", + "`quench.pieces` when you need its operator.\n", + "\n", + "## Advanced operator use\n", + "\n", + ":::{dropdown} Custom MPO tensors and construction accuracy\n", + "\n", + "Manual cores use `(left, right, physical_out, physical_in)` axes in ascending\n", + "site order. Neighboring bonds must match, exterior bonds must have dimension\n", + "one, and entries must be finite. The manual `Hamiltonian` constructor requires\n", + "uniform local dimensions. For example, these bond-one cores build the same $X_0$\n", + "operator as the matrix example:\n", + "\n", + "```python\n", + "tensor_hamiltonian = Hamiltonian(\n", + " tensors=[\n", + " pauli_x.reshape(1, 1, 2, 2),\n", + " identity.reshape(1, 1, 2, 2),\n", + " ]\n", + ")\n", + "```\n", + "\n", + "When you already have an MPO, use `Hamiltonian.from_mpo`. It references the same\n", + "MPO, checks its structure, and takes its local dimensions from the cores.\n", + "Wrapped MPOs and tensor inputs must represent a globally Hermitian operator;\n", + "YAQS does not perform a full-matrix Hermiticity check for these inputs.\n", + "\n", + "Pauli builders expose `tol`, `max_bond_dim`, and `n_sweeps` for operator\n", + "compression. Dense-to-MPO conversion also uses an SVD cutoff, so the cached MPO\n", + "can approximate the source matrix. Set conversion options explicitly with\n", + "`MPO.from_matrix(matrix, d=2, cutoff=..., max_bond=...)` and wrap the result\n", + "when you need control over this approximation. These choices concern the\n", + "operator itself; simulation-parameter presets control the evolving state.\n", + "\n", + "Static Hamiltonians cache converted forms. Call `ensure_mpo()` before reading\n", + "`.mpo`, or `ensure_sparse()` before reading `.sparse_matrix` if the form is not\n", + "already available. `to_matrix()` and `to_sparse_matrix()` return full operators\n", + "for small-system inspection. Create a new Hamiltonian when changing the\n", + "operator; cached forms do not track edits to the source data.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} Energy and long-range correlations of an MPS\n", + "\n", + "Contract an MPS with a static Hamiltonian's MPO to obtain its energy. For an\n", + "arbitrary nonzero state, divide the raw contraction by the squared norm:\n", + "\n", + "```python\n", + "hamiltonian.ensure_mpo()\n", + "norm_squared = state.mps.norm() ** 2\n", + "energy = state.mps.expect_mpo(hamiltonian.mpo) / norm_squared\n", + "```\n", + "\n", + "`expect_mpo` returns the raw complex contraction without normalizing the state.\n", + "It uses the cached MPO, including any construction approximation. A Hermitian\n", + "energy is real up to numerical error.\n", + "\n", + "The same method evaluates a full-chain Pauli product on separated sites:\n", + "\n", + "```python\n", + "correlation = MPO()\n", + "correlation.from_pauli_sum(terms=[(1.0, \"Z0 Z3\")], length=length, n_sweeps=0)\n", + "zz = state.mps.expect_mpo(correlation) / norm_squared\n", + "z0 = state.mps.expect(Observable(\"z\", 0)) / norm_squared\n", + "z3 = state.mps.expect(Observable(\"z\", 3)) / norm_squared\n", + "connected = zz - z0 * z3\n", + "```\n", + "\n", + "Here, `zz` is $\\langle Z_0Z_3\\rangle$ and `connected` subtracts\n", + "$\\langle Z_0\\rangle\\langle Z_3\\rangle$. Omitted sites act as identities. Use\n", + "`MPO.from_local_ops` instead when you already have one local matrix per site,\n", + "including identities between separated factors.\n", + "\n", + ":::\n", + "\n", + "With the operator and initial state prepared, continue with\n", + "{doc}`analog_simulation` for noisy dynamics or {doc}`simulation_parameters` for\n", + "accuracy and sampling choices. The {class}`~mqt.yaqs.Hamiltonian` API reference\n", + "lists the full constructor signatures." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 120, + "number_source_lines": true + }, + "source_map": [ + 10, + 48, + 54, + 68, + 70, + 81, + 86, + 94, + 103, + 193, + 203, + 231, + 244 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/memory_surrogate.ipynb b/docs/_outputs/examples/memory_surrogate.ipynb new file mode 100644 index 000000000..32ddaa3a6 --- /dev/null +++ b/docs/_outputs/examples/memory_surrogate.ipynb @@ -0,0 +1,1020 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Predicting Non-Markovian Dynamics\n", + "\n", + "A control pulse changes a quantum system and its later interaction with the\n", + "environment. A surrogate learns that response from simulated sequences, then\n", + "predicts reduced system states for new controls. Here we train one small model\n", + "and compare a pulse-angle sweep with exact two-qubit evolution.\n", + "\n", + "```{note}\n", + "**Experimental feature.** Surrogate modeling is not yet supported by a published\n", + "YAQS paper. This small example teaches the workflow; it does not demonstrate a\n", + "speed advantage or establish accuracy for longer control sequences. Validate\n", + "predictions against simulations or measurements for your intended use.\n", + "```\n", + "\n", + "Install the PyTorch extra with `uv pip install \"mqt.yaqs[torch]\"`. The plot also\n", + "uses Matplotlib. Run the cells in order in a notebook; for a script, use the\n", + "entry-point guard in {doc}`simulator_initialization`.\n", + "\n", + "## 1. Choose the system and train on random controls\n", + "\n", + "Site 0 is the probe, and site 1 is an environment qubit initially in\n", + "$|0\\rangle$. Their Hamiltonian is $H=-Z_0Z_1-0.5(X_0+X_1)$, with $\\hbar=1$. We\n", + "apply two random single-qubit rotations to the probe, each followed by evolution\n", + "for $0.6$. The environment can retain information about the earlier control." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import torch\n", + "\n", + "from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer\n", + "\n", + "interval = 0.6\n", + "schedule = [0.0, interval, interval]\n", + "hamiltonian = Hamiltonian.ising(2, J=1.0, g=0.5)\n", + "params = AnalogSimParams(elapsed_time=interval, dt=interval, preset=\"fast\")\n", + "characterizer = MemoryCharacterizer(show_progress=False)\n", + "\n", + "torch.manual_seed(7)\n", + "validation = characterizer.sample(\n", + " hamiltonian, params, num_interventions=2, n=128, seed=99,\n", + " timesteps=schedule, intervention_style=\"haar\",\n", + ")\n", + "model = characterizer.train(\n", + " hamiltonian, params, num_interventions=2, n=2048, seed=7,\n", + " timesteps=schedule, intervention_style=\"haar\",\n", + " model_kwargs={\"d_model\": 64, \"num_layers\": 2, \"dim_ff\": 128},\n", + " train_kwargs={\"epochs\": 200, \"lr\": 1e-3, \"device\": \"cpu\", \"val_dataset\": validation},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "`timesteps` contains the initial delay and one duration after each intervention.\n", + "The initial `0.0` places the first rotation immediately after preparation, and\n", + "the final time is $1.2$. Training varies the probe preparation and draws random\n", + "unitaries with `intervention_style=\"haar\"`; the environment preparation stays\n", + "fixed. YAQS restores the model with the lowest validation loss. The validation\n", + "set selects the model, so it is not an independent accuracy test.\n", + "\n", + "## 2. Predict a pulse-angle sweep\n", + "\n", + "Prepare the probe in $|+\\rangle$. Use the identity as the first control, then\n", + "apply $R_z(\\theta)$ between the two evolution intervals. These chosen controls\n", + "were not supplied during training. At $\\theta=0$ the system evolves freely;\n", + "$\\theta=\\pi$ gives a phase flip halfway through." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "plus = np.array([1, 1], dtype=complex) / np.sqrt(2)\n", + "rho0 = np.outer(plus, plus.conj())\n", + "identity = np.eye(2, dtype=complex)\n", + "pulse_angles = np.linspace(0, 2 * np.pi, 41)\n", + "sequences = [\n", + " [\n", + " {\"unitary\": identity},\n", + " {\"unitary\": np.diag(np.exp(-0.5j * angle * np.array([1, -1])))},\n", + " ]\n", + " for angle in pulse_angles\n", + "]\n", + "predicted = np.stack([\n", + " characterizer.predict(model, rho0, sequence) for sequence in sequences\n", + "])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "`predict` returns a complex `(2, 2)` density-matrix estimate. Stacking the sweep\n", + "produces shape `(41, 2, 2)`. All entries describe the same final time, not a\n", + "trajectory through time.\n", + "\n", + "(short-horizon-validation)=\n", + "\n", + "## 3. Check against exact evolution\n", + "\n", + "For two qubits, SciPy's matrix exponential gives a cheap reference independent\n", + "of YAQS's solvers. Site 0 is the least significant bit, so the joint initial\n", + "state is $|0\\rangle_{\\mathrm{env}}\\otimes|+\\rangle_{\\mathrm{probe}}$ and a probe\n", + "pulse acts as $I\\otimes R_z(\\theta)$. Tracing out the environment gives the\n", + "reference probe state." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Maximum matrix error: 0.0823\n", + "Maximum trace error: 0.0137; minimum eigenvalue: 0.1237\n" + ] + } + ], + "source": [ + "from scipy.linalg import expm\n", + "\n", + "pauli_x = np.array([[0, 1], [1, 0]], dtype=complex)\n", + "pauli_z = np.diag([1.0, -1.0])\n", + "dense_hamiltonian = -np.kron(pauli_z, pauli_z) - 0.5 * (\n", + " np.kron(identity, pauli_x) + np.kron(pauli_x, identity)\n", + ")\n", + "evolution = expm(-1j * interval * dense_hamiltonian)\n", + "initial_joint = np.kron([1, 0], plus)\n", + "states = []\n", + "for sequence in sequences:\n", + " joint = evolution @ np.kron(identity, sequence[1][\"unitary\"]) @ evolution @ initial_joint\n", + " amplitudes = joint.reshape(2, 2)\n", + " states.append(amplitudes.T @ amplitudes.conj())\n", + "reference = np.stack(states)\n", + "\n", + "matrix_errors = 0.5 * np.sum(np.abs(np.linalg.eigvalsh(predicted - reference)), axis=1)\n", + "trace_error = np.max(np.abs(np.trace(predicted, axis1=1, axis2=2) - 1))\n", + "minimum_eigenvalue = np.min(np.linalg.eigvalsh(predicted))\n", + "print(f\"Maximum matrix error: {matrix_errors.max():.4f}\")\n", + "print(f\"Maximum trace error: {trace_error:.4f}; minimum eigenvalue: {minimum_eigenvalue:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "The matrix error is half the trace norm of the difference. It equals trace\n", + "distance when both matrices are normalized physical states. The public API makes\n", + "predictions Hermitian but does not enforce unit trace or positivity. The printed\n", + "checks expose those limitations; we do not normalize the predictions or clip\n", + "negative eigenvalues." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:38:27.366336\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " P\n", + " u\n", + " l\n", + " s\n", + " e\n", + "  \n", + " a\n", + " n\n", + " g\n", + " l\n", + " e\n", + "  \n", + " θ\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.7\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " F\n", + " i\n", + " n\n", + " a\n", + " l\n", + "  \n", + " c\n", + " o\n", + " h\n", + " e\n", + " r\n", + " e\n", + " n\n", + " c\n", + " e\n", + "  \n", + " 2\n", + " |\n", + " |\n", + " ρ\n", + " 0\n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) Response to a control pulse\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Surrogate\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Exact evolution\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " P\n", + " u\n", + " l\n", + " s\n", + " e\n", + "  \n", + " a\n", + " n\n", + " g\n", + " l\n", + " e\n", + "  \n", + " θ\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.02\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.04\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.06\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.08\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " M\n", + " a\n", + " t\n", + " r\n", + " i\n", + " x\n", + "  \n", + " e\n", + " r\n", + " r\n", + " o\n", + " r\n", + "  \n", + " ‖\n", + " −\n", + " ‖\n", + " 1\n", + " 2\n", + " p\n", + " r\n", + " e\n", + " d\n", + " r\n", + " e\n", + " f\n", + " 1\n", + " ρ\n", + " ρ\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b) Error against exact evolution\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\", \"font.serif\": [\"STIXGeneral\"], \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 10, \"axes.labelsize\": 11, \"axes.linewidth\": 0.7,\n", + " \"xtick.direction\": \"in\", \"ytick.direction\": \"in\",\n", + " \"xtick.top\": True, \"ytick.right\": True,\n", + " \"legend.frameon\": False, \"figure.constrained_layout.use\": True,\n", + " \"savefig.bbox\": \"tight\", \"svg.fonttype\": \"none\",\n", + "})\n", + "fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.8))\n", + "axes[0].plot(pulse_angles, 2 * np.abs(predicted[:, 0, 1]), color=\"#0072B2\",\n", + " linewidth=2, label=\"Surrogate\")\n", + "axes[0].plot(pulse_angles[::2], 2 * np.abs(reference[::2, 0, 1]), \"o\", color=\"#D55E00\",\n", + " markerfacecolor=\"white\", markersize=4, label=\"Exact evolution\")\n", + "axes[0].set_ylabel(r\"Final coherence $2|\\rho_{01}|$\")\n", + "axes[0].set_title(\"(a) Response to a control pulse\", loc=\"left\", fontsize=10)\n", + "axes[0].legend(fontsize=9)\n", + "axes[1].plot(pulse_angles, matrix_errors, color=\"#0072B2\", linewidth=2)\n", + "axes[1].fill_between(pulse_angles, 0, matrix_errors, color=\"#0072B2\", alpha=0.12)\n", + "axes[1].set_ylabel(r\"Matrix error $\\frac{1}{2}\\|\\rho_{\\rm pred}-\\rho_{\\rm ref}\\|_1$\")\n", + "axes[1].set_title(\"(b) Error against exact evolution\", loc=\"left\", fontsize=10)\n", + "axes[1].set_ylim(bottom=0)\n", + "for ax in axes:\n", + " ax.set(xlabel=r\"Pulse angle $\\theta$\", xlim=(0, 2 * np.pi),\n", + " xticks=[0, np.pi, 2 * np.pi], xticklabels=[\"0\", r\"$\\pi$\", r\"$2\\pi$\"])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "An instantaneous $Z$ rotation preserves coherence magnitude when applied. The\n", + "variation here arises during subsequent interaction with the environment. The\n", + "reference and error panel show how well this small model captures that response.\n", + "This training budget keeps the example inexpensive and leaves visible prediction\n", + "errors. Results can vary with seeds and PyTorch versions.\n", + "\n", + "## Scope and other options\n", + "\n", + "The model uses a fixed Hamiltonian, environment preparation, and control\n", + "schedule. Retrain and validate when those change. The public training path does\n", + "not accept a `NoiseModel`, and this two-intervention example does not establish\n", + "accuracy for longer protocols.\n", + "\n", + "Use `predict(model, rho0, sequence, return_sequence=True)` to obtain shape\n", + "`(num_interventions, 2, 2)`, with one state after each intervention and its\n", + "following evolution interval. Sampling and training also support `\"clifford\"`\n", + "and `\"measure_prepare\"` controls; validate other control families separately.\n", + "For environmental memory diagnostics and short process-tensor references, see\n", + "{doc}`characterization`." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 120, + "number_source_lines": true + }, + "source_map": [ + 10, + 37, + 60, + 76, + 91, + 107, + 129, + 137, + 168 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/quickstart.ipynb b/docs/_outputs/examples/quickstart.ipynb new file mode 100644 index 000000000..580c98fa5 --- /dev/null +++ b/docs/_outputs/examples/quickstart.ipynb @@ -0,0 +1,12241 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Quickstart\n", + "\n", + "Explore the main YAQS workflows with executable examples and their results.\n", + "After {doc}`installing YAQS <../installation>`, run the cells in a notebook. For\n", + "a standalone script, put the execution code inside an\n", + "`if __name__ == \"__main__\":` guard; see {doc}`simulator_initialization`.\n", + "\n", + "The examples use `show_progress=False` to keep the documentation quiet. Omit\n", + "this argument to see progress. Times and rates use units consistent with each\n", + "Hamiltonian, with $\\hbar=1$. Expand the plotting cells to reuse the figures." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIXGeneral\"],\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 11,\n", + " \"axes.labelsize\": 11,\n", + " \"axes.linewidth\": 0.7,\n", + " \"xtick.labelsize\": 10,\n", + " \"ytick.labelsize\": 10,\n", + " \"xtick.direction\": \"in\",\n", + " \"ytick.direction\": \"in\",\n", + " \"xtick.top\": True,\n", + " \"ytick.right\": True,\n", + " \"legend.fontsize\": 9,\n", + " \"legend.frameon\": False,\n", + " \"lines.linewidth\": 1.6,\n", + " \"lines.markersize\": 4,\n", + " \"figure.figsize\": (6.6, 3.0),\n", + " \"figure.constrained_layout.use\": True,\n", + " \"savefig.dpi\": 180,\n", + "})" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "## Noisy analog dynamics\n", + "\n", + "Compare coherent transport and damping in a **20-site XY spin chain**. YAQS uses\n", + "an MPS and averages noisy dynamics over Monte Carlo trajectories." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseModel, Observable, Simulator, State\n", + "\n", + "length = 20\n", + "center = length // 2\n", + "basis = \"0\" * center + \"1\" + \"0\" * (length - center - 1)\n", + "state = State(length, initial=\"basis\", basis_string=basis)\n", + "hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0)\n", + "params = AnalogSimParams(\n", + " observables=[Observable(\"z\", site) for site in range(length)],\n", + " elapsed_time=3.0,\n", + " dt=0.25,\n", + " num_traj=32,\n", + " preset=\"fast\",\n", + " random_seed=7,\n", + ")\n", + "relaxation_rate = 1.0\n", + "noise = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": relaxation_rate} for site in range(length)\n", + "])\n", + "\n", + "simulator = Simulator(show_progress=False)\n", + "coherent = simulator.run(state, hamiltonian, params)\n", + "dissipative = simulator.run(state, hamiltonian, params, noise)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-4", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:38:40.731158\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib.colors import PowerNorm\n", + "\n", + "occupation = np.stack([\n", + " (1 - np.asarray(result.expectation_values).real) / 2\n", + " for result in (coherent, dissipative)\n", + "])\n", + "fig, axes = plt.subplots(1, 3, figsize=(7.2, 2.8))\n", + "for ax, values, title in zip(\n", + " axes[:2], occupation, (\"(a) Noiseless\", \"(b) Damped\"), strict=True,\n", + "):\n", + " image = ax.pcolormesh(\n", + " coherent.times, np.arange(length), values, shading=\"auto\", cmap=\"cividis\",\n", + " norm=PowerNorm(0.5, vmin=0, vmax=1), rasterized=True,\n", + " )\n", + " ax.set(xlabel=r\"Time $t$\", ylabel=r\"Site $i$\")\n", + " ax.set_title(title, loc=\"left\", fontsize=11)\n", + "fig.colorbar(image, ax=list(axes[:2]), label=r\"$\\langle n_i\\rangle$\", ticks=[0, 0.1, 0.5, 1])\n", + "axes[2].plot(coherent.times, occupation[0].sum(axis=0), color=\"#0072B2\", label=\"Noiseless\")\n", + "axes[2].plot(coherent.times, occupation[1].sum(axis=0), \"o-\", color=\"#D55E00\", label=\"Damped\")\n", + "axes[2].plot(coherent.times, np.exp(-relaxation_rate * coherent.times), \"--\", color=\"0.3\", label=\"Damping law\")\n", + "axes[2].set(xlabel=r\"Time $t$\", ylabel=r\"Total excitation $\\sum_i\\langle n_i\\rangle$\", ylim=(0, 1.08))\n", + "axes[2].set_title(\"(c) Excitation loss\", loc=\"left\", fontsize=11)\n", + "axes[2].legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-5", + "metadata": {}, + "source": [ + "The occupation $n_i=(1-Z_i)/2$ shows propagation and interference. Both heatmaps\n", + "use the same color scale, which emphasizes small occupations. Damping removes\n", + "excitations; the 32-trajectory estimate fluctuates around the exponential loss\n", + "law. This low-excitation state has limited entanglement. See\n", + "{doc}`analog_simulation` and {doc}`simulation_parameters` for larger budgets and\n", + "convergence checks.\n", + "\n", + "## Noisy circuit readout\n", + "\n", + "Prepare a **16-qubit graph state** and compare noiseless and damped readout." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-6", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "from mqt.yaqs import DigitalSimParams, NoiseModel, Simulator, State\n", + "\n", + "num_qubits = 16\n", + "circuit = QuantumCircuit(num_qubits)\n", + "circuit.h(range(num_qubits))\n", + "for site in range(num_qubits - 1):\n", + " circuit.cz(site, site + 1)\n", + "circuit.measure_all()\n", + "\n", + "state = State(num_qubits, initial=\"zeros\")\n", + "params = DigitalSimParams(shots=256, preset=\"fast\", random_seed=7)\n", + "noise = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": 0.5} for site in range(num_qubits)\n", + "])\n", + "\n", + "simulator = Simulator(show_progress=False)\n", + "ideal = simulator.run(state, circuit, params)\n", + "damped = simulator.run(state, circuit, params, noise)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-7", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:38:51.375352\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "excitation_number = np.arange(num_qubits + 1)\n", + "readout_probabilities = []\n", + "fig, ax = plt.subplots(figsize=(5.4, 2.9))\n", + "for result, color, offset, label in (\n", + " (ideal, \"#0072B2\", -0.22, \"Noiseless\"),\n", + " (damped, \"#D55E00\", 0.22, \"Damped\"),\n", + "):\n", + " probability = np.zeros(num_qubits + 1)\n", + " for outcome, count in result.counts.items():\n", + " probability[outcome.bit_count()] += count / params.shots\n", + " readout_probabilities.append(probability)\n", + " ax.bar(excitation_number + offset, probability, width=0.42, color=color,\n", + " edgecolor=\"white\", linewidth=0.5, alpha=0.9, label=label)\n", + "ax.set(xlabel=\"Number of excited qubits\", ylabel=\"Measured probability\",\n", + " xlim=(-0.5, num_qubits + 0.5), xticks=np.arange(0, num_qubits + 1, 2))\n", + "ax.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "Damping shifts the readout toward fewer excitations. Each histogram summarizes\n", + "256 shots. See {doc}`circuit_shots` for bitstring counts and\n", + "{doc}`circuit_observables` for expectation values and OpenQASM input.\n", + "\n", + "## Noisy Analog-Digital Simulation\n", + "\n", + "Can digital gates reverse the spreading in an analog spin chain? Prepare one\n", + "excitation in a **20-site XY chain**, then apply $Z$ gates on even sites halfway\n", + "through the evolution. Compare free evolution, refocusing, and refocusing with\n", + "dephasing." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-9", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "from mqt.yaqs import AnalogSimParams, DigitalSimParams, Hamiltonian, NoiseModel, Observable, SimulationProgram, Simulator, State\n", + "\n", + "length = 20\n", + "center = length // 2\n", + "state = State(length, initial=\"zeros\")\n", + "hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0)\n", + "observables = [Observable(\"z\", site) for site in range(length)]\n", + "preparation = QuantumCircuit(length)\n", + "preparation.x(center)\n", + "phase_pulse = QuantumCircuit(length)\n", + "phase_pulse.z(range(0, length, 2))\n", + "\n", + "analog_params = AnalogSimParams(elapsed_time=1.5, dt=0.25, order=2, preset=\"fast\")\n", + "digital_params = DigitalSimParams(preset=\"fast\")\n", + "free_program = SimulationProgram(\n", + " [(preparation, digital_params), (hamiltonian, analog_params), (hamiltonian, analog_params)],\n", + " observables=observables, num_traj=32, random_seed=7,\n", + ")\n", + "echo_program = SimulationProgram(\n", + " [(preparation, digital_params), (hamiltonian, analog_params),\n", + " (phase_pulse, digital_params), (hamiltonian, analog_params), (phase_pulse, digital_params)],\n", + " observables=observables, num_traj=32, random_seed=7,\n", + ")\n", + "dephasing_rate = 0.2\n", + "noise = NoiseModel([\n", + " {\"name\": \"pauli_z\", \"sites\": [site], \"strength\": dephasing_rate} for site in range(length)\n", + "])\n", + "\n", + "simulator = Simulator(show_progress=False)\n", + "free_result = simulator.run(state, free_program)\n", + "echo_result = simulator.run(state, echo_program)\n", + "noisy_echo_result = simulator.run(state, echo_program, noise_model=noise)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-10", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:38:59.436639\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib.colors import PowerNorm\n", + "\n", + "fig, axes = plt.subplots(1, 3, figsize=(7.2, 2.8), sharex=True, sharey=True)\n", + "for index, (ax, result, title) in enumerate(zip(\n", + " axes, (free_result, echo_result, noisy_echo_result),\n", + " (\"(a) Free evolution\", \"(b) Refocusing\", \"(c) Noisy refocusing\"), strict=True,\n", + ")):\n", + " segments = [segment for segment in result.segment_results if segment.segment_type == \"analog\"]\n", + " times = np.concatenate([segment.times + segment.time_offset for segment in segments])\n", + " values = np.concatenate([np.asarray(segment.expectation_values) for segment in segments], axis=1)\n", + " keep = np.r_[True, np.diff(times) > 0]\n", + " occupation = (1 - values[:, keep]) / 2\n", + " image = ax.pcolormesh(\n", + " times[keep], np.arange(length), occupation, shading=\"auto\", cmap=\"cividis\",\n", + " norm=PowerNorm(0.5, vmin=0, vmax=1), rasterized=True,\n", + " )\n", + " if index > 0:\n", + " ax.axvline(1.5, color=\"white\", linestyle=\"--\", linewidth=1)\n", + " ax.set(xlabel=r\"Time $t$\", xlim=(0, 3), yticks=[0, 5, 10, 15, 19])\n", + " ax.set_title(title, loc=\"left\", fontsize=10)\n", + "axes[0].set_ylabel(r\"Site $i$\")\n", + "fig.colorbar(image, ax=list(axes), label=r\"$\\langle n_i\\rangle$\", ticks=[0, 0.1, 0.5, 1])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-11", + "metadata": {}, + "source": [ + "The phase pulse reverses the XY exchange, bringing the excitation back at $t=3$.\n", + "A final pulse restores the phase frame. Dephasing during the analog intervals\n", + "preserves excitation but disrupts refocusing; the noisy panel averages 32\n", + "trajectories at Lindblad rate $\\gamma_z=0.2$. The gates are ideal and\n", + "instantaneous. All panels share one color scale. See\n", + "{doc}`digital_analog_simulation` for the pulse mechanism, program outputs, and\n", + "noise comparisons.\n", + "\n", + "## Circuit equivalence\n", + "\n", + "Verify a transpiled circuit, then compare the effects of an added rotation and\n", + "increasing Pauli noise." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cell-12", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Equivalent: True\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from qiskit import QuantumCircuit, transpile\n", + "\n", + "from mqt.yaqs import EquivalenceChecker, NoiseModel\n", + "\n", + "circuit = QuantumCircuit(4)\n", + "circuit.h(0)\n", + "for site in range(3):\n", + " circuit.cx(site, site + 1)\n", + "decomposed = transpile(circuit, basis_gates=[\"rz\", \"sx\", \"x\", \"cx\"])\n", + "\n", + "checker = EquivalenceChecker()\n", + "print(\"Equivalent:\", checker.check(circuit, decomposed)[\"equivalent\"])\n", + "angles = np.linspace(0, np.pi, 17)\n", + "rotation_overlaps = []\n", + "for angle in angles:\n", + " perturbed = decomposed.copy()\n", + " perturbed.rz(float(angle), 0)\n", + " rotation_overlaps.append(checker.check(circuit, perturbed)[\"fidelity\"])\n", + "\n", + "error_probabilities = np.linspace(0, 0.9, 10)\n", + "noisy_checks = []\n", + "for probability in error_probabilities:\n", + " noise = NoiseModel([{\"name\": \"pauli_z\", \"sites\": [0], \"strength\": float(probability)}])\n", + " noisy_checks.append(checker.check(\n", + " circuit, decomposed, noise_model=noise, num_traj=128, random_seed=7,\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cell-13", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:39:05.252290\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(6.8, 2.9), sharey=True)\n", + "axes[0].plot(angles, rotation_overlaps, \"o\", color=\"#0072B2\", label=\"YAQS\")\n", + "axes[0].plot(angles, np.abs(np.cos(angles / 2)), \"--\", color=\"0.3\", label=r\"$|\\cos(\\theta/2)|$\")\n", + "axes[0].set(xlabel=r\"Added rotation $\\theta$ (rad)\", ylabel=\"Normalized overlap\",\n", + " xlim=(-0.03, np.pi + 0.03), ylim=(0, 1.05), xticks=[0, np.pi / 2, np.pi],\n", + " xticklabels=[\"0\", r\"$\\pi/2$\", r\"$\\pi$\"])\n", + "axes[0].set_title(\"(a) Coherent error\", loc=\"left\", fontsize=11)\n", + "axes[0].legend()\n", + "axes[1].errorbar(error_probabilities, [check[\"fidelity\"] for check in noisy_checks],\n", + " yerr=[check[\"fidelity_error\"] for check in noisy_checks],\n", + " fmt=\"o\", color=\"#D55E00\", capsize=2, label=\"YAQS\")\n", + "axes[1].plot(error_probabilities, np.sqrt(1 - error_probabilities), \"--\", color=\"0.3\", label=r\"$\\sqrt{1-p}$\")\n", + "axes[1].set(xlabel=r\"Pauli error probability $p$\", xlim=(-0.03, 0.93))\n", + "axes[1].set_title(\"(b) Stochastic error\", loc=\"left\", fontsize=11)\n", + "axes[1].legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "An overlap of one indicates agreement up to a global phase. Here, site 0 has one\n", + "noise opportunity, after the first CX gate. A $Z$ error has zero overlap, so the\n", + "ensemble's root-mean-square overlap is $\\sqrt{1-p}$. Error bars show Monte Carlo\n", + "standard errors. Noise is applied to the second circuit. See\n", + "{doc}`equivalence_checking` for supported noise and accuracy controls.\n", + "\n", + "## Environmental memory\n", + "\n", + "Sweep the Ising coupling in a three-spin chain and compare the probe qubit's\n", + "memory spectra using the same probe grid." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "cell-15", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from mqt.yaqs import AnalogSimParams, Hamiltonian, MemoryCharacterizer\n", + "\n", + "couplings = np.linspace(0, 1.5, 13)\n", + "params = AnalogSimParams(elapsed_time=0.5, dt=0.5, preset=\"fast\")\n", + "characterizer = MemoryCharacterizer(show_progress=False)\n", + "memories = []\n", + "for coupling in couplings:\n", + " hamiltonian = Hamiltonian.ising(3, J=coupling, g=1.0)\n", + " memories.append(characterizer.characterize(\n", + " hamiltonian, params, num_interventions=4, cut=2, preset=\"quick\",\n", + " rng=np.random.default_rng(7),\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cell-16", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:39:12.268844\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(6.8, 3.0), width_ratios=[1.4, 1])\n", + "colors = plt.colormaps[\"Reds\"](np.linspace(0.35, 0.95, len(couplings)))\n", + "for memory, color in zip(memories, colors, strict=True):\n", + " spectrum = memory.singular_values(2)\n", + " weights = spectrum**2 / np.sum(spectrum**2)\n", + " axes[0].semilogy(np.arange(1, len(weights) + 1), weights, \"o-\",\n", + " color=color, linewidth=1.2, markersize=3)\n", + "axes[0].text(0.28, 0.14, \"Coupling increases\", transform=axes[0].transAxes,\n", + " ha=\"center\", va=\"center\", fontsize=10, color=\"black\")\n", + "axes[0].annotate(\"\", xy=(0.82, 0.76), xytext=(0.50, 0.20),\n", + " xycoords=\"axes fraction\",\n", + " arrowprops={\"arrowstyle\": \"->\", \"color\": \"black\",\n", + " \"lw\": 1.5, \"connectionstyle\": \"arc3,rad=0.25\"})\n", + "axes[0].set(xlabel=\"Mode index\", ylabel=r\"Resolved mode weight $p_k$\", ylim=(1e-13, 2))\n", + "axes[0].set_title(\"(a) Memory spectra\", loc=\"left\", fontsize=11)\n", + "entropies = np.array([memory.entropy(2) for memory in memories])\n", + "axes[1].fill_between(couplings, entropies, color=colors[3], alpha=0.3)\n", + "axes[1].plot(couplings, entropies, \"o-\", color=colors[-1], linewidth=2.6,\n", + " markerfacecolor=\"white\", markeredgewidth=1.2)\n", + "axes[1].set(xlabel=r\"Coupling $J$\", ylabel=r\"Memory entropy $S_V$\",\n", + " xlim=(-0.03, 1.53), ylim=(-0.008, 0.34), xticks=[0, 0.5, 1, 1.5])\n", + "axes[1].grid(axis=\"y\", color=\"0.9\", linewidth=0.5)\n", + "axes[1].set_axisbelow(True)\n", + "axes[1].set_title(\"(b) Resolved memory\", loc=\"left\", fontsize=11)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-17", + "metadata": {}, + "source": [ + "The weights $p_k=s_k^2/\\sum_j s_j^2$ describe memory resolved by the sampled\n", + "probes. Darker curves show larger $J$. Coupling redistributes weight among the\n", + "resolved modes. The entropy measures this spread and peaks within this sweep.\n", + "These weights are not environment-state populations. See {doc}`characterization`\n", + "for probe choices and interpretation.\n", + "\n", + "## Create a digital twin\n", + "\n", + "Learn\n", + "**relaxation and dephasing from measurements at the ends of a spin chain**. Then\n", + "rerun the fitted model to predict transport through the unmeasured interior." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "cell-18", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fitted rates: [0.35 0.12]\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseCharacterizer, NoiseModel, Observable, Simulator, State\n", + "\n", + "length = 4\n", + "state = State(length, initial=\"basis\", basis_string=\"1000\", representation=\"density_matrix\")\n", + "hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0)\n", + "observables = [Observable(\"z\", site) for site in range(length)]\n", + "params = AnalogSimParams(observables=observables, elapsed_time=8.0, dt=0.1, preset=\"fast\")\n", + "reference = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [3], \"strength\": 0.35},\n", + " {\"name\": \"pauli_z\", \"sites\": [2], \"strength\": 0.12},\n", + "])\n", + "guess = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [3], \"strength\": 0.2},\n", + " {\"name\": \"pauli_z\", \"sites\": [2], \"strength\": 0.2},\n", + "])\n", + "\n", + "simulator = Simulator(show_progress=False)\n", + "measured = simulator.run(state, hamiltonian, params, reference)\n", + "characterizer = NoiseCharacterizer(show_progress=False)\n", + "fit = characterizer.characterize(\n", + " hamiltonian,\n", + " params,\n", + " init_state=state,\n", + " init_guess=guess,\n", + " observables=[observables[0], observables[-1]],\n", + " ref_expectations=np.asarray(measured.expectation_values)[[0, -1]],\n", + " x_low=np.zeros(2),\n", + " x_up=np.ones(2),\n", + " max_iter=40,\n", + " seed=7,\n", + ")\n", + "reconstructed = simulator.run(state, hamiltonian, params, fit.optimal_model)\n", + "print(\"Fitted rates:\", fit.best_parameters.round(3))" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "cell-19", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:39:20.391718\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "reference_dynamics = (1 - np.asarray(measured.expectation_values).real) / 2\n", + "fitted_dynamics = (1 - np.asarray(reconstructed.expectation_values).real) / 2\n", + "fig, axes = plt.subplots(1, 2, figsize=(6.8, 2.8), sharey=True)\n", + "for ax, dynamics, title in zip(\n", + " axes, (reference_dynamics, fitted_dynamics),\n", + " (\"(a) Reference transport\", \"(b) Fitted model\"), strict=True,\n", + "):\n", + " image = ax.pcolormesh(measured.times, np.arange(length), dynamics,\n", + " shading=\"auto\", cmap=\"cividis\", vmin=0, vmax=1, rasterized=True)\n", + " ax.set(xlabel=r\"Time $t$\", yticks=np.arange(length))\n", + " ax.set_title(title, loc=\"left\", fontsize=11)\n", + "axes[0].set_ylabel(r\"Site $i$\")\n", + "fig.colorbar(image, ax=list(axes), label=r\"$\\langle n_i\\rangle$\", ticks=[0, 0.5, 1])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-20", + "metadata": {}, + "source": [ + "The excitation propagates and reflects while a local sink removes population and\n", + "dephasing changes transport. Only sites 0 and 3 enter the fit; sites 1 and 2\n", + "check its predictions. Both heatmaps share a scale. This example uses synthetic\n", + "observations without measurement noise from density-matrix simulation; replace\n", + "the reference array with your measured traces. The fit assumes the two channel\n", + "types and their sites are known. See {doc}`digital_twin` for data preparation\n", + "and validation.\n", + "\n", + "## Predict non-Markovian dynamics\n", + "\n", + "YAQS also provides experimental surrogate models that learn a probe's response\n", + "to controls from simulated training sequences. See {doc}`memory_surrogate` for a\n", + "small example with an independent reference. This feature is not yet supported\n", + "by a published YAQS paper; validate predictions for your controls and time\n", + "horizon.\n", + "\n", + "## Next steps\n", + "\n", + "| Task | Guide |\n", + "| -------------------------------------- | -------------------------------- |\n", + "| Choose accuracy settings | {doc}`simulation_parameters` |\n", + "| Choose states and simulation backends | {doc}`state_initialization` |\n", + "| Combine analog evolution and circuits | {doc}`digital_analog_simulation` |\n", + "| Check whether two circuits agree | {doc}`equivalence_checking` |\n", + "| Create a digital twin | {doc}`digital_twin` |\n", + "| Study memory in a system's environment | {doc}`characterization` |\n", + "| Predict non-Markovian dynamics | {doc}`memory_surrogate` |" + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 180, + "number_source_lines": true + }, + "source_map": [ + 10, + 23, + 51, + 58, + 84, + 110, + 123, + 146, + 165, + 178, + 215, + 240, + 255, + 285, + 303, + 316, + 333, + 360, + 374, + 412, + 428 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/realistic_noise_models.ipynb b/docs/_outputs/examples/realistic_noise_models.ipynb new file mode 100644 index 000000000..bb72c4fff --- /dev/null +++ b/docs/_outputs/examples/realistic_noise_models.ipynb @@ -0,0 +1,525 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Noise Models\n", + "\n", + "A {class}`~mqt.yaqs.NoiseModel` describes how a system loses energy, gains\n", + "excitations, or suffers other disturbances during a simulation. Assemble named\n", + "jump operators or supply your own matrices, then pass the model to\n", + "`Simulator.run`. Use fixed strengths for a calibrated model, or distributions to\n", + "represent static variation between runs.\n", + "\n", + "## Define the noise processes\n", + "\n", + "Each process has a `name`, a list of `sites`, and a `strength`. Site indices\n", + "start at zero. This four-qubit model combines local relaxation and dephasing:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import NoiseModel\n", + "\n", + "length = 4\n", + "noise = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": 0.1}\n", + " for site in range(length)\n", + "] + [\n", + " {\"name\": \"pauli_z\", \"sites\": [site], \"strength\": 0.02}\n", + " for site in range(length)\n", + "])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "The named operators act on qubits. Names are case-sensitive:\n", + "\n", + "| Process name | Effect | Sites |\n", + "| ------------------------------------- | --------------------------------------------------------------------- | --------------------------------- |\n", + "| `\"lowering\"` | Relaxation from $\\lvert1\\rangle$ to $\\lvert0\\rangle$. | One. |\n", + "| `\"raising\"` | Excitation from $\\lvert0\\rangle$ to $\\lvert1\\rangle$. | One. |\n", + "| `\"pauli_x\"`, `\"pauli_y\"`, `\"pauli_z\"` | Pauli errors; $Z$ causes dephasing. Aliases: `\"x\"`, `\"y\"`, `\"z\"`. | One. |\n", + "| `\"lowering_two\"`, `\"raising_two\"` | Joint relaxation $\\lvert11\\rangle\\to\\lvert00\\rangle$, or the reverse. | Two adjacent sites. |\n", + "| `\"crosstalk_xx\"`, `\"crosstalk_xy\"`, … | Correlated Pauli errors; any pair of `x`, `y`, and `z`. | Two; see the support table below. |\n", + "\n", + "Joint relaxation is one two-site jump. To model independent relaxation on two\n", + "sites, supply two `\"lowering\"` processes instead.\n", + "\n", + "## Interpret the strengths\n", + "\n", + "For `Simulator`, `strength` is a finite, nonnegative Lindblad rate $\\gamma$. Use\n", + "inverse units of the simulation time. YAQS supplies the factor $\\sqrt{\\gamma}$;\n", + "pass the unscaled operator as `matrix`. Operators need not be Hermitian or\n", + "unitary, and YAQS does not normalize them.\n", + "\n", + "Analog noise acts over the physical time steps set by `AnalogSimParams.dt`.\n", + "Circuit noise uses a unit noise step after each multi-qubit gate, with only the\n", + "processes whose sites lie within that gate's qubits. Single-qubit gates,\n", + "barriers, and idle qubits do not create noise opportunities. Circuit strengths\n", + "therefore do not represent hardware gate durations or direct error\n", + "probabilities. See {doc}`circuit_observables` for a worked example.\n", + "\n", + ":::{important}\n", + "`EquivalenceChecker` interprets resolved strengths as direct per-opportunity\n", + "branch probabilities and imposes its own probability constraints. A simulator\n", + "rate cannot be reused as a checker probability without choosing a conversion.\n", + "See {ref}`equivalence-noise-model`.\n", + ":::\n", + "\n", + ":::{dropdown} Relate rates to relaxation and dephasing times\n", + "For a jump operator $L$, the rate multiplies the dissipator\n", + "\n", + "$$\n", + "\\gamma\\mathcal{D}[L](\\rho)=\\gamma\\left(\n", + "L\\rho L^\\dagger-\\tfrac12\\{L^\\dagger L,\\rho\\}\\right).\n", + "$$\n", + "\n", + "With only `\"lowering\"` and no Hamiltonian, the excited-state population decays\n", + "as $e^{-\\gamma t}$, so $\\gamma=1/T_1$. With only `\"pauli_z\"`, the off-diagonal\n", + "density-matrix entries decay as $e^{-2\\gamma t}$, so $\\gamma=1/(2T_\\phi)$. These\n", + "conventions matter when converting measured lifetimes to strengths. Scaling $L$\n", + "by a factor $c$ scales its dissipator by $|c|^2$.\n", + "\n", + "Negative rates, including time-local descriptions with temporarily negative\n", + "coefficients, are not supported. Negative or complex matrix entries remain valid\n", + "parts of a jump operator.\n", + ":::\n", + "\n", + "## Run and inspect the model\n", + "\n", + "Pass the model with the initial state, Hamiltonian, and simulation parameters.\n", + "This short Ising evolution uses the default MPS representation and averages\n", + "eight trajectories:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(5,)\n" + ] + } + ], + "source": [ + "from mqt.yaqs import AnalogSimParams, Hamiltonian, Observable, Simulator, State\n", + "\n", + "state = State(length, initial=\"ones\")\n", + "hamiltonian = Hamiltonian.ising(length, J=1.0, g=0.5)\n", + "params = AnalogSimParams(\n", + " observables=[Observable(\"z\", site) for site in range(length)],\n", + " elapsed_time=0.2,\n", + " dt=0.05,\n", + " num_traj=8,\n", + " random_seed=7,\n", + ")\n", + "\n", + "sim = Simulator(show_progress=False)\n", + "result = sim.run(state, hamiltonian, params, noise_model=noise)\n", + "print(result.expectation_values[0].shape)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "The shape is `(5,)`: five sampled times, including the initial time.\n", + "`result.expectation_values` contains one such array per observable, in the\n", + "supplied order. Parallel execution remains enabled; the documentation hides\n", + "progress bars. Eight trajectories suffice to demonstrate the call, but\n", + "scientific results need a convergence check. The model used in the run is\n", + "available as `result.noise_model`.\n", + "\n", + "## Represent static variation\n", + "\n", + "Replace a scalar strength with a distribution dictionary. This example gives\n", + "each qubit an independent log-normal relaxation rate:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.10004921824250348, 0.1126931232496979, 0.08961431240104942, 0.0700306813124216]\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "variable_noise = NoiseModel([\n", + " {\n", + " \"name\": \"lowering\",\n", + " \"sites\": [site],\n", + " \"strength\": {\"distribution\": \"lognormal\", \"mean\": np.log(0.1), \"std\": 0.4},\n", + " }\n", + " for site in range(length)\n", + "])\n", + "\n", + "resolved_noise = variable_noise.sample(rng=7)\n", + "print([process[\"strength\"] for process in resolved_noise.processes])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "{meth}`~mqt.yaqs.NoiseModel.sample` returns a new model with concrete rates; it\n", + "leaves the original model unchanged. For `\"lognormal\"`, `mean` and `std`\n", + "describe the normal distribution of $\\log\\gamma$, so the median rate above is\n", + "`0.1`.\n", + "\n", + "| `distribution` | Meaning of `mean` and `std` | Treatment of negative draws |\n", + "| -------------------- | ---------------------------------------------------------- | --------------------------------------------------------------- |\n", + "| `\"lognormal\"` | Mean and standard deviation of $\\log\\gamma$. | All draws are positive. |\n", + "| `\"normal\"` | Mean and standard deviation of a normal rate distribution. | Clamped to zero with a warning. |\n", + "| `\"truncated_normal\"` | Parameters of the underlying normal distribution. | Sampled from that distribution restricted to nonnegative rates. |\n", + "\n", + "Passing `variable_noise` directly to `Simulator.run` draws one rate per process\n", + "at the start of the run. All trajectories share those rates. This represents\n", + "static disorder: rates do not change during the evolution or between\n", + "trajectories. To average over disorder, perform separate runs with separate\n", + "draws; increasing `num_traj` only improves the trajectory average for one draw.\n", + "\n", + "To compare setups using the same rates, pass a resolved model:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.10004921824250348, 0.1126931232496979, 0.08961431240104942, 0.0700306813124216]\n" + ] + } + ], + "source": [ + "resolved_result = sim.run(state, hamiltonian, params, noise_model=resolved_noise)\n", + "print([process[\"strength\"] for process in resolved_result.noise_model.processes])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "The printed rates match the earlier sample. You can also reuse a previous run's\n", + "`result.noise_model`. Setting `params.random_seed` fixes automatic disorder\n", + "draws and trajectory random streams for a fixed setup. It does not seed\n", + "independently prepared random states or final shot sampling, or guarantee\n", + "identical numbers across software versions and platforms. For successive manual\n", + "disorder draws, reuse a NumPy `Generator`; repeating `.sample(rng=7)` repeats\n", + "the same draw.\n", + "\n", + ":::{dropdown} Distribution parameters and limits\n", + "`mean` and `std` default to zero when omitted; specify both to make the intended\n", + "distribution clear. `mean` must be finite, and `std` must be finite and\n", + "nonnegative. A zero-width normal or truncated normal resolves to `max(0, mean)`;\n", + "a zero-width log-normal resolves to `exp(mean)`.\n", + "\n", + "There is no default distribution or upper rate limit. Choose a distribution from\n", + "the variation you intend to model, and choose the time step to resolve the\n", + "resulting dynamics. Clamping a normal distribution creates a point mass at zero;\n", + "truncating it renormalizes the positive part instead. Neither choice creates a\n", + "time-dependent noise model.\n", + ":::\n", + "\n", + "(noise-custom-operators)=\n", + "\n", + "## Supply a custom operator\n", + "\n", + "Add `matrix` to override the library lookup; `name` then serves as an\n", + "identifier. A one-site operator must be a finite, square matrix matching that\n", + "site's local dimension. This explicit matrix gives the same jump as\n", + "`\"lowering\"`:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": {}, + "outputs": [], + "source": [ + "sigma_minus = np.array([[0, 1], [0, 0]], dtype=complex)\n", + "custom_noise = NoiseModel([\n", + " {\"name\": \"relaxation\", \"sites\": [0], \"strength\": 0.1, \"matrix\": sigma_minus},\n", + " {\"name\": \"pauli_z\", \"sites\": [1], \"strength\": 0.02},\n", + "])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "You can mix custom and named processes. For higher local dimensions, supply an\n", + "operator in that local basis. A truncated oscillator with levels $0,1,2$ uses\n", + "the annihilation operator" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [], + "source": [ + "annihilation = np.diag(np.sqrt(np.arange(1, 3)), k=1)\n", + "qutrit_noise = NoiseModel([\n", + " {\"name\": \"loss\", \"sites\": [0], \"strength\": 0.1, \"matrix\": annihilation},\n", + "])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "This matrix is $3\\times3$ and requires a dimension-three site. See\n", + "{doc}`transmon_emulation` for a device example with such operators.\n", + "\n", + "## Choose a supported combination\n", + "\n", + "The state representation selects the analog backend. One-site and adjacent\n", + "two-site process matrices work with all three analog representations. Long-range\n", + "processes need the choices below:\n", + "\n", + "| Workflow | One-site and adjacent two-site noise | Non-adjacent two-site noise |\n", + "| -------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------- |\n", + "| Analog MPS (TJM) | Supported. | Products of Pauli operators, up to a unit-modulus phase per factor. |\n", + "| Analog vector (MCWF) | Supported. | Custom factor pairs supported. |\n", + "| Analog density matrix (Lindblad) | Supported. | Custom factor pairs supported. |\n", + "| Circuit simulation (MPS) | Supported on the gate qubits at each noise opportunity. | Not supported. |\n", + "\n", + "All operators must match the site's local dimension. Named Pauli operators are\n", + "$2\\times2$; supply custom matrices for other dimensions. YAQS checks dimensions\n", + "and site bounds against the state when the simulation starts. See\n", + "{doc}`representation_comparison` to choose a representation.\n", + "\n", + "Noisy MPS, vector, and circuit runs cannot retain a final pure state with\n", + "`get_state=True`; noisy density-matrix runs can retain their final mixed state.\n", + "The analog `list[State]` ensemble workflow does not support process noise. See\n", + "{doc}`simulation_parameters` for output choices.\n", + "\n", + ":::{dropdown} Construct adjacent and long-range two-site processes\n", + "For adjacent sites, supply a `matrix` acting on their joint basis. Use ascending\n", + "site order: the first tensor factor acts on the first listed site. If a matrix\n", + "was written for descending sites, swap both its input and output tensor-factor\n", + "axes as well as reversing the site list.\n", + "\n", + "```python\n", + "adjacent_noise = NoiseModel([\n", + " {\n", + " \"name\": \"joint_relaxation\",\n", + " \"sites\": [0, 1],\n", + " \"strength\": 0.05,\n", + " \"matrix\": np.kron(sigma_minus, sigma_minus),\n", + " },\n", + "])\n", + "```\n", + "\n", + "For non-adjacent sites, the names `\"crosstalk_xy\"` and\n", + "`\"longrange_crosstalk_xy\"` both construct the factors $X$ and $Y$. Any pair of\n", + "`x`, `y`, and `z` is accepted:\n", + "\n", + "```python\n", + "long_range_pauli = NoiseModel([\n", + " {\"name\": \"longrange_crosstalk_xy\", \"sites\": [0, 3], \"strength\": 0.05},\n", + "])\n", + "```\n", + "\n", + "For a custom non-adjacent process, supply two local `factors` in the order of\n", + "the listed sites. This lowering-and-dephasing product requires an analog vector\n", + "or density-matrix simulation:\n", + "\n", + "```python\n", + "long_range_custom = NoiseModel([\n", + " {\n", + " \"name\": \"correlated_loss\",\n", + " \"sites\": [0, 3],\n", + " \"strength\": 0.05,\n", + " \"factors\": (sigma_minus, np.diag([1, -1])),\n", + " },\n", + "])\n", + "```\n", + "\n", + "YAQS sorts the sites and reorders the factors together. Use `matrix` for\n", + "adjacent sites and `factors` for non-adjacent sites; do not provide both.\n", + ":::\n", + "\n", + "(noise-scheduled-jumps)=\n", + "\n", + "## Apply a scheduled jump\n", + "\n", + "Use `scheduled_jumps` for an operator applied at a specified analog time. Each\n", + "entry gives a `time`, `sites`, and a library `name`, or a custom `matrix` with\n", + "an identifying name. Scheduled events have no `strength`: the operator is\n", + "applied when the simulation reaches its time.\n", + "\n", + "This four-site example isolates the event by setting the Hamiltonian to zero. An\n", + "$X$ flip at $t=0.1$ changes $\\langle Z_0\\rangle$ from $+1$ to $-1$:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[ 1. 1. -1. -1. -1.]\n" + ] + } + ], + "source": [ + "scheduled_noise = NoiseModel(scheduled_jumps=[\n", + " {\"time\": 0.1, \"sites\": [0], \"name\": \"x\"},\n", + "])\n", + "jump_state = State(length, initial=\"zeros\")\n", + "zero_hamiltonian = Hamiltonian.ising(length, J=0.0, g=0.0)\n", + "jump_params = AnalogSimParams(\n", + " observables=[Observable(\"z\", 0)],\n", + " elapsed_time=0.2,\n", + " dt=0.05,\n", + " num_traj=1,\n", + " order=1,\n", + ")\n", + "jump_result = sim.run(jump_state, zero_hamiltonian, jump_params, noise_model=scheduled_noise)\n", + "print(jump_result.expectation_values[0])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "The five values are `[1, 1, -1, -1, -1]`, sampled at times\n", + "`[0, 0.05, 0.1, 0.15, 0.2]`. Event-only runs are deterministic, so one\n", + "trajectory suffices; they can also retain the final MPS with `get_state=True`.\n", + "\n", + "Scheduled jumps require a single MPS `State` and `AnalogSimParams(order=1)`.\n", + "MCWF, Lindblad, order-2 TJM, circuit runs, and `list[State]` ensembles do not\n", + "support them. Event times must lie on the simulation time grid, including its\n", + "endpoints. Two-site events must act on adjacent sites; custom matrices require\n", + "ascending site order.\n", + "\n", + ":::{important}\n", + "At a matching time after zero, scheduled operators replace the ordinary\n", + "stochastic-jump draw for that step; process dissipation still runs. For control\n", + "pulses that also sample stochastic noise at pulse times, use an\n", + "{doc}`analog-digital program `.\n", + ":::\n", + "\n", + ":::{dropdown} Custom scheduled operators and event order\n", + "Add `matrix` to supply a custom operator. For example, this schedules a $\\pi/2$\n", + "rotation about $Y$:\n", + "\n", + "```python\n", + "ry_pi2 = np.array([[1, -1], [1, 1]], dtype=complex) / np.sqrt(2)\n", + "custom_event = NoiseModel(\n", + " scheduled_jumps=[\n", + " {\"time\": 0.1, \"sites\": [0], \"name\": \"ry_pi2\", \"matrix\": ry_pi2},\n", + " ]\n", + ")\n", + "```\n", + "\n", + "Matrices must be finite, square, and match the selected sites. Scheduled events\n", + "accept `matrix`, not long-range `factors`. Operators need not be unitary; YAQS\n", + "normalizes the state after the events and rejects a zero or nonfinite norm. A\n", + "non-unitary scheduled operator is a prescribed normalized state update, not a\n", + "randomly sampled Lindblad channel.\n", + "\n", + "At time zero, events act before dissipation and the initial measurement. At\n", + "later grid points, including the final time, the order is Hamiltonian evolution,\n", + "process dissipation, all matching scheduled operators, normalization, then\n", + "measurement. Events at the same time follow their order in `scheduled_jumps`.\n", + "Inside a `SimulationProgram`, event times use the local clock of an analog run;\n", + "see {doc}`digital_analog_simulation` for segment boundaries and noise overrides.\n", + ":::\n", + "\n", + "## Next steps\n", + "\n", + "Use {doc}`analog_simulation` or {doc}`circuit_observables` to see how noise\n", + "changes measured dynamics. The device guides show how to combine noise with\n", + "{doc}`transmon_emulation` and {doc}`trapped_ion`. For deterministic control\n", + "sequences that combine gates with analog evolution, see\n", + "{doc}`digital_analog_simulation`." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 120, + "number_source_lines": true + }, + "source_map": [ + 10, + 25, + 36, + 97, + 113, + 127, + 141, + 162, + 165, + 197, + 203, + 209, + 214, + 300, + 315 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/representation_comparison.ipynb b/docs/_outputs/examples/representation_comparison.ipynb new file mode 100644 index 000000000..44dd98289 --- /dev/null +++ b/docs/_outputs/examples/representation_comparison.ipynb @@ -0,0 +1,3992 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# State Representations\n", + "\n", + "The choice of state representation determines which solver YAQS uses and how\n", + "large a system you can study. Matrix product states keep large simulations\n", + "manageable when entanglement remains limited. Dense vectors and density matrices\n", + "provide useful small-system references, with different costs for noisy dynamics.\n", + "\n", + "Choose the representation when constructing a {class}`~mqt.yaqs.State`. YAQS\n", + "selects the solver from that choice; it does not switch representations when the\n", + "calculation becomes expensive.\n", + "\n", + "## Choose a representation and solver\n", + "\n", + "| `State` representation | Analog solver | When to use it |\n", + "| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------------------ |\n", + "| `\"mps\"` (default) | Tensor jump method (TJM). | Large chains with manageable entanglement, and circuit simulation. |\n", + "| `\"vector\"` | Monte Carlo wave-function method (MCWF). | Small systems where you want pure-state trajectories without MPS compression. |\n", + "| `\"density_matrix\"` | Lindblad master equation. | Small open systems, mixed initial states, and deterministic ensemble averages. |\n", + "\n", + "For presets, use `State(length, initial=\"zeros\", representation=\"vector\")`, for\n", + "example. Supplying `vector=`, `density_matrix=`, or `tensors=` selects the\n", + "matching representation automatically. See {doc}`state_initialization` for state\n", + "preparation and local dimensions.\n", + "\n", + "With noise, TJM and MCWF evolve independent pure-state trajectories and average\n", + "their observables. Lindblad evolution propagates the ensemble density matrix\n", + "directly. Without noise, the trajectory solvers evolve a single pure state;\n", + "Lindblad evolution still carries a density matrix.\n", + "\n", + "## How state storage scales\n", + "\n", + "Let $L$ be the number of sites, $d$ their common local dimension, and $D=d^L$\n", + "the full Hilbert-space dimension. For qubits, $d=2$. An MPS stores local tensors\n", + "joined by bonds; the maximum bond dimension $\\chi$ controls how much\n", + "entanglement it can represent.\n", + "\n", + "| Representation | Complex numbers in one state | For qubits |\n", + "| -------------- | ---------------------------- | ------------------------------------ |\n", + "| MPS | $O(Ld\\chi^2)$. | Linear in $L$ if $\\chi$ stays fixed. |\n", + "| Vector | $D$. | $2^L$. |\n", + "| Density matrix | $D^2$. | $4^L$. |\n", + "\n", + "Each additional qubit doubles vector storage and quadruples density-matrix\n", + "storage. Increasing an MPS bond dimension by a factor of two can increase its\n", + "state storage by about four. For strongly entangled states, the bond dimension\n", + "needed for an accurate MPS can itself grow exponentially with system size;\n", + "linear scaling at fixed bond dimension is not a guarantee for every physical\n", + "problem. See the\n", + "[TJM publication](https://www.nature.com/articles/s41467-025-66846-x) for the\n", + "trajectory formulation.\n", + "\n", + "The figure counts `complex128` state arrays, at 16 bytes per entry. The MPS\n", + "curves use bond caps of 16 and 64, with each bond also limited by the dimensions\n", + "of the two subsystems it separates. These are calculated storage estimates; no\n", + "large states are allocated." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:39:28.850630\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\",\n", + " \"font.serif\": [\"STIXGeneral\"],\n", + " \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 11,\n", + " \"axes.labelsize\": 11,\n", + " \"axes.linewidth\": 0.7,\n", + " \"xtick.labelsize\": 10,\n", + " \"ytick.labelsize\": 10,\n", + " \"xtick.direction\": \"in\",\n", + " \"ytick.direction\": \"in\",\n", + " \"xtick.top\": True,\n", + " \"ytick.right\": True,\n", + " \"legend.fontsize\": 9,\n", + " \"legend.frameon\": False,\n", + " \"lines.linewidth\": 1.8,\n", + " \"figure.constrained_layout.use\": True,\n", + " \"savefig.dpi\": 180,\n", + "})\n", + "\n", + "qubits = np.arange(4, 41)\n", + "vector_bytes = 16 * 2.0**qubits\n", + "density_bytes = 16 * 4.0**qubits\n", + "mps_bytes = {}\n", + "for cap in (16, 64):\n", + " sizes = []\n", + " for sites in qubits:\n", + " bonds = [min(cap, 2**min(cut, sites - cut)) for cut in range(sites + 1)]\n", + " sizes.append(16 * sum(2 * left * right for left, right in zip(bonds[:-1], bonds[1:], strict=True)))\n", + " mps_bytes[cap] = np.array(sizes)\n", + "\n", + "fig, ax = plt.subplots(figsize=(6.6, 3.6))\n", + "ax.semilogy(qubits, density_bytes, color=\"0.2\", label=r\"Density matrix: $4^L$\")\n", + "ax.semilogy(qubits, vector_bytes, color=\"#D55E00\", label=r\"Vector: $2^L$\")\n", + "ax.semilogy(qubits, mps_bytes[64], color=\"#0072B2\", label=r\"MPS: $\\chi\\leq64$\")\n", + "ax.semilogy(qubits, mps_bytes[16], \"--\", color=\"#56B4E9\", label=r\"MPS: $\\chi\\leq16$\")\n", + "ax.axhline(2.0**30, color=\"0.6\", linewidth=0.8, linestyle=\":\")\n", + "ax.text(39, 2.0**30 * 1.6, \"1 GiB reference\", ha=\"right\", fontsize=9, color=\"0.35\")\n", + "ax.set(xlabel=r\"Number of qubits $L$\", ylabel=\"State storage\", xlim=(4, 40), ylim=(2.0**8, 2.0**64))\n", + "ax.set_xticks([4, 10, 20, 30, 40])\n", + "ax.set_yticks([2.0**10, 2.0**20, 2.0**30, 2.0**40, 2.0**50, 2.0**60],\n", + " labels=[\"1 KiB\", \"1 MiB\", \"1 GiB\", \"1 TiB\", \"1 PiB\", \"1 EiB\"])\n", + "ax.legend(loc=\"lower left\", bbox_to_anchor=(0, 1.02), ncols=2, borderaxespad=0)\n", + "ax.grid(axis=\"y\", alpha=0.15)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "The plot shows one state's arrays, not peak solver memory or a practical qubit\n", + "limit. The density-matrix curve continues above the plotted range. Hamiltonians,\n", + "jump operators, temporary arrays, worker copies, and saved observable\n", + "trajectories also use memory. In particular, a cached propagator can be much\n", + "larger than the state it evolves.\n", + "\n", + "## How solver work scales\n", + "\n", + "For an MPS with fixed local dimension, Hamiltonian MPO bond dimension, and local\n", + "solver effort, a TDVP sweep costs roughly $O(L\\chi^3)$. The MPO bond dimension\n", + "measures the size of the Hamiltonian's tensor-network representation. More\n", + "complex interactions and growing state bonds increase the work. This estimate\n", + "describes local TDVP sweeps, not every supported integrator or noise pattern.\n", + "\n", + "Dense solvers have two regimes in the current implementation:\n", + "\n", + "| Solver | Small-system method | Method above the cache threshold |\n", + "| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |\n", + "| Vector / MCWF | Build a dense $D\\times D$ step propagator: roughly $O(D^3)$ setup, $O(D^2)$ storage and work per step. | Apply the exponential through sparse Krylov methods; work depends on operator nonzeros and iteration count. |\n", + "| Density matrix / Lindblad | Build a dense $D^2\\times D^2$ generator and step propagator: roughly $O(D^6)$ exponential setup, $O(D^4)$ storage and work per step. | Integrate the matrix master equation with adaptive RK45, without storing the full generator. |\n", + "\n", + "For a fixed number of Krylov iterations and short-range sparse operators, vector\n", + "evolution typically needs order $LD$ work per exponential action. For a\n", + "Hamiltonian with order $LD$ nonzeros and $K$ local jump channels, one Lindblad\n", + "derivative evaluation costs order $(L+K)D^2$. The number of adaptive steps\n", + "depends on the dynamics and tolerances, so these estimates do not give a single\n", + "runtime law for every problem.\n", + "\n", + ":::{dropdown} Propagator thresholds and other memory costs\n", + "The vector solver caches its dense step propagator when `D <= 4096` (up to 12\n", + "qubits). The density-matrix solver builds a dense generator and caches its step\n", + "propagator when `D**2 <= 4096` (up to 6 qubits). At either upper threshold, one\n", + "propagator alone occupies 256 MiB in `complex128`; preprocessing needs\n", + "additional arrays. These are implementation thresholds, not recommended system\n", + "sizes or user memory limits.\n", + "\n", + "MPS evolution also stores the Hamiltonian MPO and contraction environments. For\n", + "uniform dimensions and an MPO bond dimension $w$, their typical storage is\n", + "$O(Ld^2w^2)$ and $O(Lw\\chi^2)$, respectively. Intermediate bonds can exceed the\n", + "final retained bonds during an update.\n", + "\n", + "For unequal local dimensions, replace $D=d^L$ with $D=\\prod_i d_i$. The exact\n", + "MPS state entry count is $\\sum_i d_i\\chi_i\\chi_{i+1}$, where the end bonds have\n", + "dimension one. Local dimensions above two can raise costs sharply even when the\n", + "number of sites stays fixed.\n", + ":::\n", + "\n", + "### Trajectories, time steps, and parallelism\n", + "\n", + "Noisy TJM and MCWF work grows approximately in proportion to `num_traj` and the\n", + "number of evolution steps, at fixed numerical settings. Standard Monte Carlo\n", + "error decreases as $1/\\sqrt{N_{\\mathrm{traj}}}$: halving that error generally\n", + "requires four times as many trajectories. Increasing the trajectory count does\n", + "not remove timestep or MPS approximation errors.\n", + "\n", + "Lindblad evolution needs one deterministic run, so increasing `num_traj` has no\n", + "effect. Parallel workers can reduce the wall time of trajectory ensembles, but\n", + "they do not reduce the total numerical work and can increase memory use. There\n", + "is no universal fastest representation; the balance depends on the model,\n", + "accuracy, and hardware. The\n", + "[computational-regimes study](https://arxiv.org/abs/2606.13779) compares\n", + "trajectory costs and sampling effort.\n", + "\n", + "## Compare the same noisy dynamics\n", + "\n", + "A four-site version of the XY chain in {doc}`analog_simulation` provides a small\n", + "comparison. One excitation starts at site 1, moves between neighbors, and can be\n", + "lost through local relaxation. We measure the occupation of its starting site,\n", + "$n_1=(1-Z_1)/2$, using identical physical inputs for all three solvers." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseModel, Observable, Simulator, State\n", + "\n", + "length = 4\n", + "initial_site = 1\n", + "basis = \"0\" * initial_site + \"1\" + \"0\" * (length - initial_site - 1)\n", + "hamiltonian = Hamiltonian.heisenberg(length, Jx=0.5, Jy=0.5, Jz=0.0)\n", + "noise = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": 0.6}\n", + " for site in range(length)\n", + "])\n", + "params = AnalogSimParams(\n", + " observables=[Observable(\"z\", initial_site)],\n", + " elapsed_time=1.0,\n", + " dt=0.05,\n", + " num_traj=64,\n", + " random_seed=7,\n", + ")\n", + "sim = Simulator(show_progress=False)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "Only the `State` representation changes between runs. Product-state presets\n", + "create the same initial physical state directly in each representation, so no\n", + "manual tensor copying or dense conversion is needed:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "results = {}\n", + "for representation in (\"density_matrix\", \"vector\", \"mps\"):\n", + " state = State(length, initial=\"basis\", basis_string=basis, representation=representation)\n", + " results[representation] = sim.run(state, hamiltonian, params, noise_model=noise)\n", + "\n", + "times = results[\"density_matrix\"].times" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "Parallel execution remains enabled, and the documentation hides progress bars.\n", + "Run the cells in order in a notebook; see {doc}`simulator_initialization` for\n", + "the main guard required in a script. The MPS and vector results each store an\n", + "array of means in `expectation_values[0]` and an array of shape `(64, 21)` in\n", + "`trajectories[0]`. The density-matrix result has a single deterministic row." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:39:35.827397\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(6.6, 3.2))\n", + "reference = (1 - results[\"density_matrix\"].expectation_values[0]) / 2\n", + "ax.plot(times, reference, color=\"0.2\", label=\"Lindblad reference\")\n", + "for representation, color, marker, style, offset, label in (\n", + " (\"mps\", \"#0072B2\", \"o\", \"-\", 0, \"TJM / MPS\"),\n", + " (\"vector\", \"#D55E00\", \"s\", \"--\", 1, \"MCWF / vector\"),\n", + "):\n", + " result = results[representation]\n", + " mean = (1 - result.expectation_values[0]) / 2\n", + " samples = (1 - result.trajectories[0]) / 2\n", + " standard_error = samples.std(axis=0, ddof=1) / np.sqrt(samples.shape[0])\n", + " ax.fill_between(times, mean - standard_error, mean + standard_error, color=color, alpha=0.14, linewidth=0)\n", + " ax.plot(times, mean, color=color, marker=marker, linestyle=style,\n", + " markevery=(offset, 2), markersize=3.2, markerfacecolor=\"white\",\n", + " linewidth=1.2, label=label)\n", + "ax.set(xlabel=r\"Time $t$\", ylabel=r\"Occupation $\\langle n_1\\rangle$\", xlim=(0, 1), ylim=(-0.03, 1.04))\n", + "ax.legend(loc=\"upper right\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "The lines show excitation transport combined with loss. Shaded bands give one\n", + "estimated standard error of the trajectory means at each time; they are not\n", + "bounds on the numerical error or simultaneous confidence bands. Sixty-four\n", + "trajectories illustrate statistical variation, not a converged benchmark or a\n", + "runtime comparison. The shared seed can correlate the two trajectory estimates.\n", + "Lindblad supplies a deterministic numerical reference, not an error-free\n", + "solution.\n", + "\n", + "## Check accuracy and supported workflows\n", + "\n", + "For noisy MPS and vector results, increase `num_traj` to test sampling error,\n", + "then reduce `dt` to check time resolution. For MPS, also increase `max_bond_dim`\n", + "and reduce `svd_threshold` to check compression. A product initial state can\n", + "become entangled during evolution; its initial simplicity does not make the\n", + "later MPS approximation exact. Use {doc}`simulation_parameters` for presets and\n", + "solver-specific controls.\n", + "\n", + "| Workflow or input | Supported representations |\n", + "| ---------------------------------------- | --------------------------------------------------------------------------------------------------- |\n", + "| Static-Hamiltonian analog evolution | MPS, vector, and density matrix; noise must meet the restrictions in {doc}`realistic_noise_models`. |\n", + "| Circuits and analog-digital programs | MPS. |\n", + "| Mixed initial state | Density matrix. |\n", + "| Bitstring-probability observables | MPS with qubits at every site. |\n", + "| Entropy and Schmidt-spectrum observables | MPS; noisy results describe pure trajectories, not the spectrum of a mixed density matrix. |\n", + "| Piecewise Hamiltonian | MPS with TDVP; see {doc}`hamiltonians`. |\n", + "| Unitary `list[State]` ensemble | MPS; see {doc}`ensemble_evolution`. |\n", + "\n", + "Noisy MPS, vector, and circuit runs do not return one final pure state for the\n", + "trajectory ensemble. Noisy density-matrix evolution can retain its final mixed\n", + "state with `get_state=True`.\n", + "\n", + "Choose MPS when the required bond dimensions remain affordable, vector when a\n", + "full pure state fits and provides a useful reference, and density matrix when\n", + "you need a small-system ensemble average or mixed-state evolution. Check\n", + "convergence for the quantities you intend to report before scaling up." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 120, + "number_source_lines": true + }, + "source_map": [ + 10, + 68, + 120, + 192, + 211, + 217, + 224, + 232, + 252 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/simulation_parameters.ipynb b/docs/_outputs/examples/simulation_parameters.ipynb new file mode 100644 index 000000000..e6bc3011e --- /dev/null +++ b/docs/_outputs/examples/simulation_parameters.ipynb @@ -0,0 +1,357 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Configuring Simulation Parameters\n", + "\n", + "Simulation parameters choose what to measure, when to record it, and the\n", + "numerical accuracy. Start with a preset, then change the settings that matter\n", + "for your calculation.\n", + "\n", + "| Class | Use for |\n", + "| ------------------ | ---------------------------------------------------------------------- |\n", + "| `AnalogSimParams` | Hamiltonian evolution, including noisy dynamics and unitary ensembles. |\n", + "| `DigitalSimParams` | Circuit expectation values, shot counts, or a final state. |\n", + "\n", + "Pass the parameter object to `Simulator.run` with the initial state and\n", + "Hamiltonian or circuit. Supply noise through `noise_model` in that call. The\n", + "state representation selects the analog backend; see\n", + "{doc}`representation_comparison`. Parallel execution and progress controls\n", + "belong to `Simulator`, as described in {doc}`simulator_initialization`.\n", + "\n", + "## Choose a preset\n", + "\n", + "Both classes default to `preset=\"balanced\"`. A preset supplies four settings:\n", + "\n", + "| Preset | `svd_threshold` | `max_bond_dim` | `num_traj` | `krylov_tol` |\n", + "| ------------ | --------------- | -------------- | ---------- | ------------ |\n", + "| `\"fast\"` | `1e-3` | `16` | `128` | `1e-3` |\n", + "| `\"balanced\"` | `1e-6` | `128` | `256` | `1e-4` |\n", + "| `\"accurate\"` | `1e-9` | `4096` | `1024` | `1e-6` |\n", + "| `\"exact\"` | `1e-13` | `None` | `1024` | `1e-12` |\n", + "\n", + "Use `\"fast\"` to explore a model and `\"balanced\"` as a starting point for\n", + "accuracy checks. `\"accurate\"` tightens the numerical settings and increases the\n", + "trajectory budget, at greater cost. `\"exact\"` provides strict reference settings\n", + "and removes the bond cap; finite timesteps, truncation, and sampling error still\n", + "apply. No preset establishes convergence for every model.\n", + "\n", + "Explicit constructor arguments replace only the corresponding preset values:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import AnalogSimParams, DigitalSimParams, Observable\n", + "\n", + "custom_params = AnalogSimParams(\n", + " preset=\"accurate\",\n", + " max_bond_dim=256,\n", + " num_traj=64,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "Here, `svd_threshold` and `krylov_tol` retain their `\"accurate\"` values. Omit\n", + "`max_bond_dim` to keep the preset cap; pass `None` explicitly to remove it.\n", + "Choose the preset when constructing the object. Assigning a new value to\n", + "`params.preset` later does not reset the other fields. `shots` is always an\n", + "explicit budget and is not part of a preset.\n", + "\n", + "## Set analog measurements and times\n", + "\n", + "For a four-site chain, record the local Pauli $Z$ expectations over two time\n", + "units:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "analog_params = AnalogSimParams(\n", + " observables=[Observable(\"z\", site) for site in range(4)],\n", + " elapsed_time=2.0,\n", + " dt=0.05,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "The default `sample_timesteps=True` records 41 samples, including time zero and\n", + "the final time. After the run, `result.times` holds the sampled times and\n", + "`result.expectation_values[i]` holds the values for the $i$th supplied\n", + "observable. Set `sample_timesteps=False` to keep only the final sample;\n", + "evolution still uses the same `dt`.\n", + "\n", + "The timestep must be positive, and the duration must be non-negative and an\n", + "integer multiple of `dt`. For a computed grid, set\n", + "`elapsed_time = num_steps * dt` to keep the duration consistent with the step\n", + "count. Invalid grids raise `ValueError`.\n", + "\n", + "For noisy trajectory evolution, `num_traj` sets the number of realizations to\n", + "average. A noiseless single-state run uses one realization. A density-matrix\n", + "backend evolves the ensemble directly, while a supplied `list[State]` uses the\n", + "list length as its ensemble size. See {doc}`analog_simulation` for a noisy\n", + "walkthrough and {doc}`ensemble_evolution` for unitary ensemble averages.\n", + "\n", + "## Choose circuit outputs\n", + "\n", + "Request observables, shots, or both. At least one output must be requested in a\n", + "standalone run; `get_state=True` is another option for supported noiseless runs." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "observable_params = DigitalSimParams(observables=[Observable(\"z\", 0)])\n", + "shot_params = DigitalSimParams(shots=1024)\n", + "combined_params = DigitalSimParams(\n", + " observables=[Observable(\"z\", 0)],\n", + " shots=1024,\n", + " num_traj=64,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "Observables are recorded at the circuit's end by default. With\n", + "`sample_layers=True`, YAQS also records the initial state and checkpoints marked\n", + "by `circuit.barrier(label=\"SAMPLE_OBSERVABLES\")`. It does not sample every\n", + "circuit layer automatically. See {doc}`circuit_observables` for checkpoint\n", + "placement and the resulting sample axis.\n", + "\n", + "`shots` is the total readout budget. `num_traj` controls the noisy observable\n", + "ensemble, so the two settings have different roles:\n", + "\n", + "| Run | How YAQS uses the budgets |\n", + "| ----------------------- | ------------------------------------------------------------------------------- |\n", + "| Noiseless | Evolve once and sample all requested shots from the final state. |\n", + "| Noisy, with observables | Average `num_traj` trajectories. Distribute any requested shots across them. |\n", + "| Noisy, shots only | Run one single-shot trajectory per shot; `num_traj` does not control this path. |\n", + "\n", + "A combined run supports `shots < num_traj`: every trajectory contributes to the\n", + "observable mean, while some receive no readout samples. Counts appear in\n", + "`result.counts` as integer keys and integer counts. Site 0 is the\n", + "least-significant bit, matching Qiskit's `int(bitstring, 2)` convention. See\n", + "{doc}`circuit_shots` for histogram plotting.\n", + "\n", + "## Choose observables and diagnostics\n", + "\n", + "Named operators avoid imports from gate libraries. The Pauli names below refer\n", + "to $\\sigma^\\alpha$, rather than spin operators $S^\\alpha=\\sigma^\\alpha/2$.\n", + "\n", + "| Request | Example |\n", + "| --------------------------------- | ---------------------------------------------------------------------- |\n", + "| Single-site Pauli operator | `Observable(\"z\", 0)`; also `\"x\"` and `\"y\"` |\n", + "| Identity or basis projector | `Observable(\"id\", 0)`, `Observable(\"p0\", 0)`, or `Observable(\"p1\", 0)` |\n", + "| Two-site Pauli operator | `Observable(\"zz\", [0, 1])`; also `\"xx\"` and `\"yy\"` |\n", + "| Position on a supplied grid | `Observable(\"position\", 0, positions=grid)` |\n", + "| Probability of a full basis state | `Observable(\"0101\")` for a four-qubit system |\n", + "| Custom Hermitian operator | `Observable(matrix, sites=0)` or `Observable(matrix, sites=[0, 1])` |\n", + "\n", + "Two-site local operators support adjacent sites and the periodic end-to-end bond\n", + "on qubit chains. Matrix factors follow the supplied site order. Bitstring\n", + "probabilities require an all-qubit MPS and cannot share an observable list with\n", + "ordinary operators or entanglement diagnostics. Shot counts can accompany\n", + "ordinary observables.\n", + "\n", + "For MPS entanglement across the bond between sites 1 and 2, request the adjacent\n", + "pair:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [], + "source": [ + "diagnostic_params = AnalogSimParams(\n", + " observables=[\n", + " Observable(\"entropy\", sites=[1, 2]),\n", + " Observable(\"schmidt_spectrum\", sites=[1, 2]),\n", + " ],\n", + " elapsed_time=2.0,\n", + " dt=0.05,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "These diagnostics work at sampled times or circuit checkpoints, including noisy\n", + "MPS runs. Spectra contain descending Schmidt coefficients, with up to 500\n", + "entries and `NaN` padding. With noise, YAQS averages the pure-trajectory\n", + "entropies and coefficients; these are not the entropy or spectrum of the\n", + "ensemble's mixed density matrix. Missing ranks contribute zero to coefficient\n", + "means, while ranks absent from every trajectory remain `NaN`.\n", + "\n", + "## Refine accuracy and retain results\n", + "\n", + "Change one source of error at a time and compare the observable that matters for\n", + "your calculation:\n", + "\n", + "| Setting | When to change it |\n", + "| --------------- | ---------------------------------------------------------------------------------------------------------- |\n", + "| `dt` | Reduce it at fixed analog duration to check time-step error. |\n", + "| `num_traj` | Increase it to reduce uncertainty in noisy trajectory averages. |\n", + "| `max_bond_dim` | Increase the MPS bond cap if it limits the evolving state. Larger bonds cost memory and time. |\n", + "| `svd_threshold` | Reduce it to retain more information during tensor truncation. |\n", + "| `krylov_tol` | Reduce it to tighten local matrix-exponential solves in TDVP or BUG. This does not tighten SVD truncation. |\n", + "\n", + "Keep an explicit trajectory budget when comparing presets, since a preset\n", + "changes that budget along with the numerical settings. Tensor truncation and\n", + "integrator controls concern MPS evolution; dense backends use different\n", + "numerical methods. See {doc}`representation_comparison` before changing them.\n", + "\n", + "Set `random_seed` to a non-negative integer to repeat jump decisions and sampled\n", + "static disorder for the same input and configuration. It does not seed random\n", + "state initialization or measurement-shot sampling.\n", + "\n", + "Set `get_state=True` to retain a supported final state in `result.output_state`.\n", + "Noisy MPS, statevector, and circuit runs cannot return one final pure state for\n", + "the trajectory ensemble. Noisy density-matrix evolution can return its final\n", + "mixed state. Unitary list-of-state ensembles do not return a final ensemble\n", + "state.\n", + "\n", + "## Advanced numerical options\n", + "\n", + ":::{dropdown} Analog integrators and two-time correlations\n", + "\n", + "MPS analog evolution defaults to `evolution_mode=EvolutionMode.TDVP`. Import\n", + "`EvolutionMode` from `mqt.yaqs` and select `EvolutionMode.BUG` to use the BUG\n", + "integrator. See {doc}`analog_simulation` for the workflow.\n", + "\n", + "`order=1` or `order=2` selects the TJM splitting order for noisy MPS evolution.\n", + "This is separate from `tdvp_mode`, which controls the TDVP state updates:\n", + "\n", + "- `\"2site\"` (default) allows bond growth through two-site updates.\n", + "- `\"1site\"` uses single-site updates with fixed bond dimensions.\n", + "- `\"dynamic\"` switches between single-site and two-site updates.\n", + "\n", + "`tdvp_sweeps` defaults to 1. Increasing it subdivides each TDVP evolution step\n", + "into symmetric substeps with the same total evolution time. In noisy analog\n", + "runs, noise still acts on the full physical timestep `dt`; extra TDVP substeps\n", + "do not refine that noise timestep.\n", + "\n", + "For two-time correlations, pass `multi_time_observables=[(A, B)]` with a\n", + "noiseless MPS `list[State]` and a static Hamiltonian. `B` acts at time zero and\n", + "`A` at the later time. The complex results appear in `multi_time_results`, with\n", + "one row per pair. See {doc}`ensemble_evolution` for the definition and example.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} Circuit gate-application modes\n", + "\n", + "Keep `gate_mode=\"mpo\"` unless you need to compare another method. It applies\n", + "gates directly and compresses the result using the configured tensor truncation.\n", + "The available modes are:\n", + "\n", + "| Mode | Two-qubit gates |\n", + "| ----------------- | ----------------------------------------------------------------------------------- |\n", + "| `\"mpo\"` (default) | Direct local updates for neighbors; an extended gate MPO for separated sites. |\n", + "| `\"swaps\"` | Route separated sites together with SWAPs, apply the gate, and restore their order. |\n", + "| `\"tdvp\"` | Direct local updates for neighbors; generator-based TDVP for separated sites. |\n", + "| `\"full-tdvp\"` | Generator-based TDVP for both neighboring and separated sites. |\n", + "\n", + "TDVP gate paths are variational approximations and can miss the bond growth an\n", + "entangling gate requires. Additional `tdvp_sweeps` may help, but do not\n", + "guarantee exact gate application. Use `\"mpo\"`, or `\"swaps\"` for two-qubit gates,\n", + "for direct application up to the configured truncation. Digital generator-based\n", + "TDVP requires `tdvp_mode=\"2site\"`.\n", + "\n", + "Matrix-backed custom gates without an analytic generator use direct local\n", + "updates for neighbors and the MPO path for separated sites, including in TDVP\n", + "modes. Gates on three or more qubits use an MPO, except supported product-form\n", + "generators such as `ccx` and `ccz` in TDVP modes. See\n", + "{ref}`circuit-custom-gates` for custom gate inputs.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} SVD truncation modes\n", + "\n", + "`trunc_mode=\"discarded_weight\"` is the default. `svd_threshold` limits the sum\n", + "of discarded squared singular values. The other modes interpret the threshold as\n", + "a fraction of total squared weight (`\"relative_discarded_weight\"`), a ratio to\n", + "the largest singular value (`\"relative\"`), or an absolute singular-value cutoff\n", + "(`\"hard_cutoff\"`). `max_bond_dim` can force further truncation in every mode.\n", + "Presets do not change `trunc_mode`.\n", + "\n", + ":::\n", + "\n", + "## Related guides\n", + "\n", + "- {doc}`quickstart` — simulation and characterization workflows.\n", + "- {doc}`analog_simulation` — noisy spin dynamics and convergence checks.\n", + "- {doc}`circuit_observables` — circuit dynamics and sampling checkpoints.\n", + "- {doc}`circuit_shots` — noisy readout distributions.\n", + "- {doc}`simulator_initialization` — execution controls and result fields.\n", + "\n", + "Full constructor signatures are in the API reference for\n", + "{class}`~mqt.yaqs.AnalogSimParams` and {class}`~mqt.yaqs.DigitalSimParams`." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 48, + 56, + 69, + 75, + 99, + 107, + 153, + 162 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/simulator_initialization.ipynb b/docs/_outputs/examples/simulator_initialization.ipynb new file mode 100644 index 000000000..ca14cbd10 --- /dev/null +++ b/docs/_outputs/examples/simulator_initialization.ipynb @@ -0,0 +1,301 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Configuring the Simulator\n", + "\n", + "`Simulator` controls how a calculation runs: parallel workers, progress bars,\n", + "and worker-error handling. Construct one instance and reuse it for successive\n", + "runs. The state, Hamiltonian or circuit, measurements, and noise are supplied\n", + "with each call; their settings are described in {doc}`simulation_parameters`.\n", + "\n", + "## Run a small noisy simulation\n", + "\n", + "This four-site Ising chain starts with every spin in $|1\\rangle$. Local\n", + "relaxation acts during the evolution, and eight trajectories contribute to the\n", + "mean Pauli $Z$ expectations:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import AnalogSimParams, Hamiltonian, NoiseModel, Observable, Simulator, State\n", + "\n", + "length = 4\n", + "state = State(length, initial=\"ones\")\n", + "hamiltonian = Hamiltonian.ising(length, J=1.0, g=0.5)\n", + "params = AnalogSimParams(\n", + " observables=[Observable(\"z\", site) for site in range(length)],\n", + " elapsed_time=0.4,\n", + " dt=0.05,\n", + " num_traj=8,\n", + " random_seed=7,\n", + ")\n", + "noise = NoiseModel([\n", + " {\"name\": \"lowering\", \"sites\": [site], \"strength\": 0.4}\n", + " for site in range(length)\n", + "])\n", + "\n", + "sim = Simulator(show_progress=False)\n", + "result = sim.run(state, hamiltonian, params, noise_model=noise)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "Parallel execution remains enabled. The documentation suppresses progress bars\n", + "with `show_progress=False`; use `Simulator()` to see progress in your own runs.\n", + "The small trajectory budget illustrates execution and result access, rather than\n", + "sampling convergence.\n", + "\n", + "## Read the result and reuse the simulator\n", + "\n", + "`run` returns a `Result`. Observable order matches the supplied list:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "times = result.times\n", + "z0_mean = result.expectation_values[0]\n", + "z0_trajectories = result.trajectories[0]" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "Here, `times` and `z0_mean` have nine entries, including time zero.\n", + "`z0_trajectories` has shape `(8, 9)`: one row per trajectory and one column per\n", + "time. `z0_mean` averages those rows. Circuit runs can also return readout counts\n", + "in `result.counts`; see {doc}`circuit_shots`.\n", + "\n", + "Reuse the same simulator and inputs for a noiseless reference:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "reference = sim.run(state, hamiltonian, params)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "Each run starts from the supplied initial state. It does not continue from the\n", + "previous result. Sequential calls share execution settings, but they do not keep\n", + "a worker pool alive. YAQS creates a pool only when the calculation has multiple\n", + "independent jobs and more than one worker is available.\n", + "\n", + "## Choose the common execution controls\n", + "\n", + "All constructor options are keyword-only. Leave the defaults in place unless you\n", + "need a specific execution budget or a quieter run.\n", + "\n", + "| Option | Default | When to change it |\n", + "| --------------- | --------- | --------------------------------------------------------------------------------------- |\n", + "| `show_progress` | `True` | Set `False` to suppress trajectory and readout bars in documentation or logs. |\n", + "| `max_workers` | Automatic | Set a positive integer to cap worker processes, for example `Simulator(max_workers=4)`. |\n", + "| `parallel` | `True` | Set `False` to debug in the calling process or avoid process startup for a small job. |\n", + "\n", + "A pool requires `parallel=True`, more than one independent job, and\n", + "`max_workers > 1`. Noiseless single-state evolution and density-matrix evolution\n", + "run in the calling process. `max_workers=1` also keeps execution in that\n", + "process, even when parallel execution is enabled, and limits numerical threads\n", + "to one. Worker processes limit their own numerical threads to avoid multiplying\n", + "thread pools across CPUs.\n", + "\n", + "You can change settings between calls, for example `sim.max_workers = 2` or\n", + "`sim.show_progress = True`. Setting `sim.max_workers = None` restores automatic\n", + "worker selection.\n", + "\n", + "## Run from a Python script\n", + "\n", + "Worker processes start through `forkserver` on Linux and `spawn` on Windows and\n", + "macOS by default. These methods need a script entry-point guard so importing the\n", + "script in a child process does not start the simulation again.\n", + "\n", + "Keep the imports and input definitions from the first example. Replace its\n", + "simulator creation and run calls with this block, and keep subsequent run calls\n", + "inside the guard:\n", + "\n", + "```python\n", + "if __name__ == \"__main__\":\n", + " sim = Simulator()\n", + " result = sim.run(state, hamiltonian, params, noise_model=noise)\n", + " reference = sim.run(state, hamiltonian, params)\n", + "```\n", + "\n", + "Run the file with `python your_script.py`. In a notebook, execute the cells in\n", + "order without adding this guard. If process startup is the cause of a debugging\n", + "problem, `parallel=False` lets you inspect the calculation in the notebook's\n", + "process.\n", + "\n", + "## Advanced execution options\n", + "\n", + ":::{dropdown} Automatic worker budgets and numerical threads\n", + "\n", + "With `max_workers=None`, the simulator uses `max(1, available_cpus() - 1)`. CPU\n", + "discovery takes the first valid hint from:\n", + "\n", + "1. `YAQS_MAX_WORKERS`.\n", + "2. A pytest-xdist worker, which reports one CPU to avoid nested pools.\n", + "3. `SLURM_CPUS_PER_TASK`, then `SLURM_CPUS_ON_NODE`.\n", + "4. Process CPU affinity, when available.\n", + "5. The operating system's CPU count.\n", + "\n", + "Invalid or non-positive environment hints are ignored. The default worker policy\n", + "then leaves one reported CPU free. Thus `YAQS_MAX_WORKERS=4` normally resolves\n", + "to three workers; `Simulator(max_workers=4)` explicitly permits four. Use the\n", + "constructor argument when you need an exact process cap. CPU affinity can\n", + "restrict the available cores, but this discovery does not read every container\n", + "CPU-quota setting.\n", + "\n", + "A worker cap bounds process count, not total memory. Each process needs its own\n", + "simulation state. Reduce the cap when several concurrent trajectories exceed\n", + "your memory budget. Numerical libraries are capped inside workers; turning off\n", + "process parallelism does not promise unrestricted BLAS threading.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} Multiprocessing start methods\n", + "\n", + "`mp_context=\"auto\"` selects `\"forkserver\"` on Linux and `\"spawn\"` elsewhere. For\n", + "an explicit selection, the public options are `\"spawn\"` and `\"fork\"` on\n", + "platforms that support them. There is no fallback for an unavailable method.\n", + "Keep `\"auto\"` unless the environment requires a specific method.\n", + "\n", + "`\"spawn\"` starts a fresh interpreter for each worker. It can also be useful when\n", + "combining YAQS with libraries that need fresh process initialization. `\"fork\"`\n", + "copies the application's process and can be unsafe when the parent has active\n", + "threads. Thread limits do not remove that risk. The automatic Linux context\n", + "starts workers from a separate server process; its first pool has additional\n", + "startup cost.\n", + "\n", + "Worker inputs must be pickleable. Pools are scoped to individual runs, although\n", + "the Python forkserver helper can remain alive between calls. These execution\n", + "choices do not change the requested simulation model.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} Retries for worker errors\n", + "\n", + "`max_retries=10` allows up to ten additional attempts for a failed job in a\n", + "process pool. The default `retry_exceptions` tuple contains\n", + "`concurrent.futures.CancelledError`, `TimeoutError`, and `OSError`.\n", + "\n", + "Only matching exceptions raised when retrieving a worker result trigger a retry.\n", + "After the retry budget is exhausted, the error propagates. Other exceptions,\n", + "including `ValueError`, propagate immediately under the default policy. Set\n", + "`max_retries=0` to propagate the first worker failure, or supply a tuple of\n", + "exception classes for a specific transient failure in your environment.\n", + "\n", + "Retries do not impose a timeout, apply to in-process execution, or restart a\n", + "broken pool. Pool startup and submission failures are outside this retry policy.\n", + "Do not increase the retry budget to handle a repeatable error in the model.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} Other result fields and configuration references\n", + "\n", + "Outputs that do not apply to a run remain `None` or empty. The API reference for\n", + "{class}`~mqt.yaqs.Result` describes the full result structure:\n", + "\n", + "| Fields | Use |\n", + "| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |\n", + "| `observables`, `expectation_values`, `trajectories` | Requested observables, their means, and individual realization data in the supplied order. |\n", + "| `times` | Analog sample times, or the stitched timeline of a program with observables. Standalone circuits have no physical time axis. |\n", + "| `counts`, `measurements` | Total circuit readout counts and per-trajectory histograms when shots are requested. |\n", + "| `output_state` | A final state when `get_state=True` is supported; see {doc}`simulation_parameters`. |\n", + "| `max_bond`, `total_bond`, `runtime_cost` | MPS bond diagnostics and an estimated contraction cost, when recorded. |\n", + "| `multi_time_times`, `multi_time_results` | Complex two-time correlations for unitary ensembles; see {doc}`ensemble_evolution`. |\n", + "| `segment_results` | Individual results from a `SimulationProgram`; see {doc}`digital_analog_simulation`. |\n", + "| `sim_params`, `noise_model` | The validated simulation parameters and the noise model used by the run. |\n", + "\n", + "For a standalone run, `result.sim_params` references the supplied parameter\n", + "object. Validation normalizes that object and rebuilds its analog time grid; it\n", + "is not an immutable snapshot. Construct a new parameter object when you need to\n", + "retain a separate configuration. State and Hamiltonian wrappers may also\n", + "populate cached representations, while evolution uses copies of their input\n", + "states.\n", + "\n", + "A program's top-level `sim_params` is `None`; read segment parameters through\n", + "`result.segment_results`. Program settings and trajectory budgets are explained\n", + "in {doc}`digital_analog_simulation`.\n", + "\n", + "For temporary storage, pickle can save results for later analysis with matching\n", + "Python, YAQS, and dependency versions. This stores the result; it does not\n", + "resume an interrupted simulation. Only load pickle files from a trusted source.\n", + "\n", + ":::\n", + "\n", + "## Related guides\n", + "\n", + "- {doc}`simulation_parameters` — accuracy, sampling budgets, and output\n", + " requests.\n", + "- {doc}`analog_simulation` — noisy analog dynamics.\n", + "- {doc}`circuit_observables` — circuit expectations and checkpoints.\n", + "- {doc}`digital_analog_simulation` — analog-digital programs and segment\n", + " results.\n", + "- {doc}`representation_comparison` — choosing the analog state representation.\n", + "\n", + "Full constructor and `run` signatures are in the API reference for\n", + "{class}`~mqt.yaqs.Simulator`." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 25, + 45, + 56, + 60, + 69, + 71 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/state_initialization.ipynb b/docs/_outputs/examples/state_initialization.ipynb new file mode 100644 index 000000000..260878c93 --- /dev/null +++ b/docs/_outputs/examples/state_initialization.ipynb @@ -0,0 +1,368 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Initializing Quantum States\n", + "\n", + "The initial state sets the starting point for a YAQS simulation. Use `State` to\n", + "choose a named preparation or supply your own data. The default matrix product\n", + "state (MPS) representation supports analog and circuit simulation without\n", + "storing a full state vector.\n", + "\n", + "## Choose a product state\n", + "\n", + "Specify the number of sites and, optionally, a preset. Sites are qubits unless\n", + "you provide other local dimensions:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import State\n", + "\n", + "zeros = State(20)\n", + "polarized = State(20, initial=\"x+\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "Here, `zeros` puts every site in $|0\\rangle$, while `polarized` puts every site\n", + "in $(|0\\rangle + |1\\rangle)/\\sqrt{2}$. Both are product states: the sites have\n", + "no initial entanglement.\n", + "\n", + "| `initial` | Preparation |\n", + "| ------------------- | ------------------------------------------------------------------------------------------------ |\n", + "| `\"zeros\"` (default) | Every site in $\\lvert 0\\rangle$. |\n", + "| `\"ones\"` | Every site in $\\lvert 1\\rangle$. |\n", + "| `\"x+\"`, `\"x-\"` | Every site in $(\\lvert 0\\rangle \\pm \\lvert 1\\rangle)/\\sqrt{2}$. |\n", + "| `\"y+\"`, `\"y-\"` | Every site in $(\\lvert 0\\rangle \\pm i\\lvert 1\\rangle)/\\sqrt{2}$. |\n", + "| `\"Neel\"` | Alternating levels, starting with site 0 in $\\lvert 1\\rangle$: `1010…` in site order. |\n", + "| `\"wall\"` | The first `length // 2` sites in $\\lvert 0\\rangle$ and the remaining sites in $\\lvert 1\\rangle$. |\n", + "| `\"basis\"` | One computational-basis configuration, supplied with `basis_string`. |\n", + "| `\"random\"` | A random product state; see the random-state examples below. |\n", + "\n", + "## Place an excitation and check site order\n", + "\n", + "The {doc}`analog_simulation` guide follows an excitation that starts near the\n", + "center of a chain. Prepare that state by giving one basis digit per site:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "length = 20\n", + "center = length // 2\n", + "basis = \"0\" * center + \"1\" + \"0\" * (length - center - 1)\n", + "localized = State(length, initial=\"basis\", basis_string=basis)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "Character `i` in `basis_string` selects site `i`, starting with site 0 on the\n", + "left. A measurement bitstring instead displays site 0 on the right:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]\n" + ] + } + ], + "source": [ + "site_zero = State(3, initial=\"basis\", basis_string=\"100\")\n", + "print(site_zero.mps.to_vec())" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "This state has site 0 in $|1\\rangle$ and the other sites in $|0\\rangle$. Its\n", + "dense vector has its only nonzero entry at index 1, and its readout bitstring is\n", + "`\"001\"`. Dense vectors and the rows and columns of density matrices use site 0\n", + "as the fastest-varying subsystem. For qubits, this matches Qiskit's ordering.\n", + "\n", + "## Choose a representation\n", + "\n", + "Set `representation` when constructing a preset state, for example\n", + "`State(4, initial=\"x+\", representation=\"vector\")`. YAQS selects the analog\n", + "backend from this choice:\n", + "\n", + "| `representation` | State storage and supported use |\n", + "| ------------------ | ---------------------------------------------------------------------------------- |\n", + "| `\"mps\"` (default) | Tensor network for analog evolution, circuits, and unitary ensembles. |\n", + "| `\"vector\"` | Dense pure state for analog evolution with Monte Carlo wave-function trajectories. |\n", + "| `\"density_matrix\"` | Dense pure or mixed state for analog Lindblad evolution. |\n", + "\n", + "For $N$ qubits, a dense vector has $2^N$ entries and a density matrix has $4^N$\n", + "entries. Keep dense calculations small; circuit simulation requires `\"mps\"`. See\n", + "{doc}`representation_comparison` for a comparison on the same physical model.\n", + "\n", + "## Prepare random states\n", + "\n", + "Use `\"random\"` for an unentangled state with real, nonnegative random local\n", + "amplitudes. To allow initial entanglement, use `\"haar-random\"` and choose an\n", + "initial maximum bond dimension with `pad`:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [], + "source": [ + "random_product = State(6, initial=\"random\")\n", + "random_mps = State(6, initial=\"haar-random\", pad=4)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "`\"haar-random\"` builds an MPS from random isometries. It does not sample\n", + "uniformly from all pure states in the full Hilbert space. The bond dimensions\n", + "are limited by `pad` and the sizes of the neighboring subsystems. Omitting `pad`\n", + "gives a maximum bond dimension of one, so the state is then a product state.\n", + "This initial choice is separate from `max_bond_dim` during evolution.\n", + "\n", + "For a reproducible random product state in a dense representation, pass `seed`:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": {}, + "outputs": [], + "source": [ + "repeatable = State(4, initial=\"random\", representation=\"vector\", seed=7)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "The same seed reproduces the same initial vector. It also works with\n", + "`representation=\"density_matrix\"`, which forms the corresponding pure-state\n", + "density matrix. Currently, random MPS initialization and `\"haar-random\"` ignore\n", + "`seed`. To reuse those preparations, construct the state once and pass the same\n", + "object to successive runs. The simulation parameter `random_seed` controls\n", + "stochastic evolution; it does not seed state preparation.\n", + "\n", + "## Supply a vector or a mixed state\n", + "\n", + "Pass exactly one of `vector`, `density_matrix`, or `tensors` for manual data.\n", + "The representation is inferred, so omit `representation`. Do not combine manual\n", + "data with preset options such as `initial`, `basis_string`, `seed`, or `pad`.\n", + "\n", + "The vector below prepares the entangled Bell state\n", + "$(|00\\rangle + |11\\rangle)/\\sqrt{2}$:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.70710678+0.j 0. +0.j 0. +0.j 0.70710678+0.j]\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "bell = State(vector=np.array([1, 0, 0, 1], dtype=complex))\n", + "print(bell.vector)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "YAQS copies and normalizes the vector. The input must be a finite, nonzero,\n", + "one-dimensional array. Without explicit local dimensions, its size must be a\n", + "power of two; YAQS infers the number of qubits.\n", + "\n", + "A mixed state describes a statistical preparation. This example puts the system\n", + "in $|00\\rangle$ with probability 0.6 and $|11\\rangle$ with probability 0.4:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": {}, + "outputs": [], + "source": [ + "rho = np.diag([0.6, 0.0, 0.0, 0.4]).astype(complex)\n", + "mixed = State(density_matrix=rho)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "YAQS copies the matrix and normalizes its trace. The input must be finite,\n", + "square, Hermitian, positive semidefinite, and have positive real trace. Manual\n", + "vectors and density matrices select their dense analog backends. They cannot\n", + "serve as circuit inputs; use a preset or MPS tensors instead.\n", + "\n", + "## Use other local dimensions\n", + "\n", + "Set `physical_dimensions` to an integer for a uniform chain or a list for\n", + "different dimensions at each site:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cell-15", + "metadata": {}, + "outputs": [], + "source": [ + "qutrits = State(3, physical_dimensions=3)\n", + "qubit_qutrit = State(\n", + " 2, initial=\"basis\", basis_string=\"12\", physical_dimensions=[2, 3]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-16", + "metadata": {}, + "source": [ + "The second state has a qubit at site 0 in level 1 and a qutrit at site 1 in\n", + "level 2. Presets such as `\"ones\"` still use level 1, not the highest local\n", + "level. For manual dense data, local dimensions must multiply to the vector\n", + "length or matrix dimension. Bitstring probabilities and shot measurements\n", + "require qubits; digital gates currently require qubit target sites. See\n", + "{doc}`transmon_emulation` and {doc}`trapped_ion` for device examples.\n", + "\n", + "## Advanced MPS preparation\n", + "\n", + ":::{dropdown} Supply MPS tensors or wrap an existing MPS\n", + "\n", + "Use `tensors=` for custom open-boundary MPS cores. Each core has axes\n", + "`(physical, left, right)`. Neighboring bond dimensions must match, exterior\n", + "bonds must have dimension one, and all entries must be finite. This example\n", + "prepares the same Bell state as the vector above, now in MPS form:\n", + "\n", + "```python\n", + "left = (np.eye(2) / np.sqrt(2)).reshape(2, 1, 2)\n", + "right = np.eye(2).reshape(2, 2, 1)\n", + "bell_mps = State(tensors=[left, right])\n", + "wrapped = State.from_mps(bell_mps.mps)\n", + "```\n", + "\n", + "`State(tensors=...)` infers the site count and normalizes the MPS. For other\n", + "physical dimensions, supply matching `physical_dimensions`.\n", + "\n", + "When you already have an {class}`~mqt.yaqs.MPS`, use\n", + "{meth}`~mqt.yaqs.State.from_mps` to wrap it. This method references the same MPS\n", + "without copying or normalizing it. Changes through either reference affect the\n", + "same state. For an independent normalized state, supply copies of its tensors\n", + "through `State(tensors=...)` and preserve its physical dimensions.\n", + "\n", + ":::\n", + "\n", + ":::{dropdown} Pad the initial MPS bonds\n", + "\n", + "For product presets, `pad` adds zero entries to enlarge the initial MPS bonds\n", + "without changing the physical state. For `\"haar-random\"`, it instead sets the\n", + "maximum initial bond dimension used during random construction.\n", + "\n", + "Padding allocates initial MPS space; it does not set the bond limit during\n", + "evolution. That limit is `max_bond_dim` in the simulation parameters. Dense\n", + "product-state construction does not use MPS padding.\n", + "\n", + ":::\n", + "\n", + "Once the initial state is prepared, pass it to `Simulator.run` with the model,\n", + "measurements, and optional noise. The {doc}`simulator_initialization` guide\n", + "shows how to run and reuse a simulator, while {doc}`simulation_parameters`\n", + "describes accuracy and sampling choices. Full constructor details are in the\n", + "{class}`~mqt.yaqs.State` API reference." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 24, + 29, + 51, + 56, + 61, + 64, + 93, + 96, + 106, + 108, + 126, + 131, + 140, + 143, + 155, + 160 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/transmon_emulation.ipynb b/docs/_outputs/examples/transmon_emulation.ipynb new file mode 100644 index 000000000..edcad4c13 --- /dev/null +++ b/docs/_outputs/examples/transmon_emulation.ipynb @@ -0,0 +1,4280 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Superconducting Qubit Emulation\n", + "\n", + "A resonator can carry an excitation between superconducting qubits. How much\n", + "reaches the receiving qubit, and how does noise affect the transfer? We model\n", + "two transmons coupled through a resonator, follow their populations, and compare\n", + "several relaxation and dephasing strengths. Keeping a third transmon level also\n", + "lets us track occupation outside the qubit computational subspace.\n", + "\n", + "This guide uses the standard YAQS installation and Matplotlib. Run the cells in\n", + "order in a notebook. For a script, use the entry-point guard in\n", + "{doc}`simulator_initialization`.\n", + "\n", + "## 1. Build the transmon–resonator model\n", + "\n", + "`Hamiltonian.coupled_transmon` places transmons at even sites and resonators at\n", + "odd sites. Here, sites 0 and 2 are three-level transmons, and site 1 is a\n", + "four-level resonator. Each transmon has a Duffing term\n", + "$\\omega_q n+\\alpha n(n-1)/2$, while the resonator has energy $\\omega_r n$.\n", + "Neighboring sites interact through the full dipole coupling\n", + "$g(b+b^\\dagger)(a+a^\\dagger)$, where $b$ and $a$ lower the transmon and\n", + "resonator levels." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from mqt.yaqs import Hamiltonian\n", + "\n", + "qubit_dim = 3\n", + "resonator_dim = 4\n", + "physical_dimensions = [qubit_dim, resonator_dim, qubit_dim]\n", + "coupling = 1.0\n", + "hamiltonian = Hamiltonian.coupled_transmon(\n", + " length=3,\n", + " qubit_dim=qubit_dim,\n", + " resonator_dim=resonator_dim,\n", + " qubit_freq=20.0 * coupling,\n", + " resonator_freq=20.0 * coupling,\n", + " anharmonicity=-1.5 * coupling,\n", + " coupling=coupling,\n", + ")\n", + "transfer_time = np.pi / (np.sqrt(2) * coupling)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "We use $\\hbar=1$ and measure frequencies and rates in units of $g$, so time is\n", + "in units of $1/g$. The frequency arguments enter the Hamiltonian directly:\n", + "convert ordinary frequencies to angular frequencies before supplying dimensional\n", + "values. YAQS adds no factor of $2\\pi$.\n", + "\n", + "On resonance, $T=\\pi/(\\sqrt{2}g)$ estimates the first complete transfer in the\n", + "rotating-wave approximation. The factory retains counter-rotating terms, so\n", + "transfer at this time is approximate and total excitation is not exactly\n", + "conserved. This is a model of excitation transfer; one prepared state does not\n", + "validate a SWAP gate on arbitrary inputs.\n", + "\n", + "## 2. Excite the left transmon\n", + "\n", + "Start in $|100\\rangle$: the left transmon is excited, while the resonator and\n", + "right transmon start in their ground states. Characters in `basis_string` follow\n", + "site order, starting at site 0." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import State\n", + "\n", + "state = State(\n", + " 3, initial=\"basis\", basis_string=\"100\", physical_dimensions=physical_dimensions,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "The explicit dimensions must match the Hamiltonian. YAQS uses an MPS by default\n", + "and supports different local dimensions within the same chain.\n", + "\n", + "## 3. Choose populations and the time grid\n", + "\n", + "Use local matrix observables to measure each site's $|1\\rangle$ population. On\n", + "the transmons, also measure $|2\\rangle$ population to detect leakage from the\n", + "computational subspace. Mean occupation $\\langle n\\rangle$ on all three sites\n", + "lets us track total excitation." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import AnalogSimParams, Observable\n", + "\n", + "observables = [\n", + " Observable(np.diag(np.arange(dim) == 1).astype(float), site)\n", + " for site, dim in enumerate(physical_dimensions)\n", + "]\n", + "observables += [Observable(np.diag([0.0, 0.0, 1.0]), site) for site in (0, 2)]\n", + "observables += [\n", + " Observable(np.diag(np.arange(dim)).astype(float), site)\n", + " for site, dim in enumerate(physical_dimensions)\n", + "]\n", + "params = AnalogSimParams(\n", + " observables=observables,\n", + " elapsed_time=transfer_time,\n", + " dt=transfer_time / 80,\n", + " order=2,\n", + " num_traj=24,\n", + " preset=\"balanced\",\n", + " random_seed=7,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "Each matrix matches its site's dimension. The observable order is three\n", + "$|1\\rangle$ populations, two transmon $|2\\rangle$ populations, then three mean\n", + "occupations. Binary bitstring observables and shot counts require an all-qubit\n", + "state; local matrix observables also work with higher levels.\n", + "\n", + "The grid contains 81 samples over one transfer interval. The `balanced` preset\n", + "sets numerical tolerances; `order=2` selects second-order TJM for noisy runs.\n", + "Each noisy calculation averages 24 trajectories. Sampling error, timestep error,\n", + "and the chosen level cutoffs need separate convergence checks.\n", + "\n", + "## 4. Follow the noiseless transfer\n", + "\n", + "Initialize the simulator separately and omit a noise model for the baseline.\n", + "YAQS preserves the input state, so subsequent runs can reuse the same\n", + "preparation." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Right transmon population at T: 0.996\n" + ] + } + ], + "source": [ + "from mqt.yaqs import Simulator\n", + "\n", + "simulator = Simulator(show_progress=False)\n", + "noiseless = simulator.run(state, hamiltonian, params)\n", + "times = noiseless.times\n", + "values = np.asarray(noiseless.expectation_values)\n", + "print(f\"Right transmon population at T: {values[2, -1]:.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "`values` has shape `(8, 81)`: observables by sampled times. The first three rows\n", + "show the excitation moving through the chain. Rows 3 and 4 measure leakage on\n", + "the left and right transmons. Their sum is the expected number of transmons in\n", + "$|2\\rangle$, rather than the probability that either transmon has leaked. The\n", + "resonator's higher photon states are not qubit leakage." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:39:47.238793\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " /\n", + " t\n", + " T\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " |\n", + " 1\n", + " ⟩\n", + "  \n", + " p\n", + " o\n", + " p\n", + " u\n", + " l\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) Excitation transfer\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Left transmon\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Resonator\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Right transmon\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " /\n", + " t\n", + " T\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " |\n", + " 2\n", + " ⟩\n", + "  \n", + " p\n", + " o\n", + " p\n", + " u\n", + " l\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " (\n", + " 1\n", + " 0\n", + " )\n", + " −\n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b) Transmon leakage\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Left transmon\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Right transmon\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\", \"font.serif\": [\"STIXGeneral\"], \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 10, \"axes.labelsize\": 11, \"axes.linewidth\": 0.8,\n", + " \"xtick.direction\": \"in\", \"ytick.direction\": \"in\", \"svg.fonttype\": \"none\",\n", + " \"legend.frameon\": False,\n", + "})\n", + "scaled_times = times / transfer_time\n", + "site_labels = [\"Left transmon\", \"Resonator\", \"Right transmon\"]\n", + "site_colors = [\"#D55E00\", \"0.5\", \"#0072B2\"]\n", + "fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9), layout=\"constrained\")\n", + "for site, label, color in zip(range(3), site_labels, site_colors, strict=True):\n", + " axes[0].plot(scaled_times, values[site], color=color, linewidth=1.8, label=label)\n", + "for row, label, color in ((3, \"Left transmon\", site_colors[0]), (4, \"Right transmon\", site_colors[2])):\n", + " axes[1].plot(scaled_times, 1e3 * values[row], color=color, linewidth=1.4, label=label)\n", + "axes[0].set(ylabel=r\"$|1\\rangle$ population\", ylim=(0, 1.05))\n", + "axes[1].set(ylabel=r\"$|2\\rangle$ population ($10^{-3}$)\")\n", + "for ax, title in zip(axes, [\"(a) Excitation transfer\", \"(b) Transmon leakage\"], strict=True):\n", + " ax.set(xlabel=r\"Time $t/T$\", xlim=(0, 1))\n", + " ax.set_title(title, loc=\"left\", fontsize=11)\n", + " ax.legend(fontsize=8)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "**The resonator mediates transfer between the transmons.** The resonator\n", + "population rises and falls while the receiving transmon approaches unit\n", + "population near $T$. Small, rapid excursions into $|2\\rangle$ remain visible on\n", + "the separate linear scale. The full dipole interaction permits these excursions\n", + "even from a single-excitation preparation. The plotted receiver population does\n", + "not measure the fidelity of an arbitrary transferred quantum state.\n", + "\n", + "## 5. Add multilevel relaxation and dephasing\n", + "\n", + "The built-in `lowering` and `pauli_z` channels are two-dimensional. For a\n", + "three-level transmon, supply explicit matrices. The annihilation matrix $b$\n", + "relaxes $|1\\rangle$ to $|0\\rangle$ and $|2\\rangle$ to $|1\\rangle$, with the\n", + "oscillator's $\\sqrt{2}$ matrix element. The number operator $n$ produces pure\n", + "dephasing without directly changing populations." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import NoiseModel\n", + "\n", + "lowering = np.diag(np.sqrt(np.arange(1, qubit_dim)), k=1)\n", + "number = np.diag(np.arange(qubit_dim)).astype(float)\n", + "relaxation_rates = coupling * np.array([0.05, 0.15, 0.6])\n", + "results = {0.0: noiseless}\n", + "for rate in relaxation_rates:\n", + " noise = NoiseModel(\n", + " [{\"name\": \"relaxation\", \"sites\": [site], \"strength\": rate, \"matrix\": lowering}\n", + " for site in (0, 2)]\n", + " + [{\"name\": \"dephasing\", \"sites\": [site], \"strength\": 2 * rate, \"matrix\": number}\n", + " for site in (0, 2)]\n", + " )\n", + " results[rate] = simulator.run(state, hamiltonian, params, noise)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "`strength` is a Lindblad rate: YAQS multiplies each supplied matrix by\n", + "`sqrt(strength)`. Here the jumps are $\\sqrt{\\gamma}\\,b$ and $\\sqrt{2\\gamma}\\,n$.\n", + "For an isolated transmon's $|0\\rangle$–$|1\\rangle$ transition, the relaxation\n", + "and pure-dephasing times are both $1/\\gamma$. The sweep increases both channels\n", + "together and leaves the resonator noise-free. These deliberately short coherence\n", + "times make the competition with transfer visible; the parameters are\n", + "illustrative, rather than a fit to a device.\n", + "\n", + "Parallel execution is enabled by default. The documentation suppresses progress\n", + "bars with `show_progress=False`; omit that setting to see progress. The seed\n", + "fixes random streams for repeated runs with the same configuration." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:40:18.099931\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " /\n", + " t\n", + " T\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Left\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Resonator\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Right\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) No noise\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " /\n", + " t\n", + " T\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Left\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Resonator\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Right\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (\n", + " b\n", + " )\n", + "  \n", + " /\n", + " =\n", + " 0\n", + " .\n", + " 0\n", + " 5\n", + " γ\n", + " g\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " /\n", + " t\n", + " T\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Left\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Resonator\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Right\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (\n", + " c\n", + " )\n", + "  \n", + " /\n", + " =\n", + " 0\n", + " .\n", + " 1\n", + " 5\n", + " γ\n", + " g\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " /\n", + " t\n", + " T\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Left\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Resonator\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Right\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (\n", + " d\n", + " )\n", + "  \n", + " /\n", + " =\n", + " 0\n", + " .\n", + " 6\n", + " γ\n", + " g\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " /\n", + " t\n", + " T\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.25\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.50\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.75\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " R\n", + " i\n", + " g\n", + " h\n", + " t\n", + "  \n", + " t\n", + " r\n", + " a\n", + " n\n", + " s\n", + " m\n", + " o\n", + " n\n", + "  \n", + " |\n", + " 1\n", + " ⟩\n", + "  \n", + " p\n", + " o\n", + " p\n", + " u\n", + " l\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (e) Received excitation\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " No noise\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " γ\n", + " g\n", + " /\n", + " =\n", + " 0\n", + " .\n", + " 0\n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " γ\n", + " g\n", + " /\n", + " =\n", + " 0\n", + " .\n", + " 1\n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " γ\n", + " g\n", + " /\n", + " =\n", + " 0\n", + " .\n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " /\n", + " t\n", + " T\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.25\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.50\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.75\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " o\n", + " t\n", + " a\n", + " l\n", + "  \n", + " e\n", + " x\n", + " c\n", + " i\n", + " t\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " ∑\n", + " ⟨\n", + " ⟩\n", + " i\n", + " i\n", + " n\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (f) Excitation remaining\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " |\n", + " 1\n", + " ⟩\n", + "  \n", + " p\n", + " o\n", + " p\n", + " u\n", + " l\n", + " a\n", + " t\n", + " i\n", + " o\n", + " n\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib.colors import Normalize\n", + "\n", + "colors = [\"0.2\", \"#56B4E9\", \"#0072B2\", \"#D55E00\"]\n", + "fig, axes = plt.subplots(3, 2, figsize=(7.2, 6.6), layout=\"constrained\")\n", + "for index, (ax, (rate, result)) in enumerate(zip(axes[:2].flat, results.items(), strict=True)):\n", + " means = np.asarray(result.expectation_values)\n", + " image = ax.pcolormesh(scaled_times, np.arange(3), means[:3], shading=\"auto\",\n", + " cmap=\"cividis\", norm=Normalize(0, 1), rasterized=True)\n", + " title = \"(a) No noise\" if index == 0 else rf\"({chr(97 + index)}) $\\gamma/g={rate / coupling:g}$\"\n", + " ax.set(xlabel=r\"Time $t/T$\", yticks=[0, 1, 2], yticklabels=[\"Left\", \"Resonator\", \"Right\"], xlim=(0, 1))\n", + " ax.set_title(title, loc=\"left\", fontsize=11)\n", + "fig.colorbar(image, ax=list(axes[:2].flat), label=r\"$|1\\rangle$ population\", ticks=[0, 0.5, 1], shrink=0.8)\n", + "\n", + "for (rate, result), color in zip(results.items(), colors, strict=True):\n", + " means = np.asarray(result.expectation_values)\n", + " trajectories = np.asarray(result.trajectories)\n", + " label = \"No noise\" if rate == 0 else rf\"$\\gamma/g={rate / coupling:g}$\"\n", + " for ax, mean, samples in (\n", + " (axes[2, 0], means[2], trajectories[2]),\n", + " (axes[2, 1], means[5:8].sum(axis=0), trajectories[5:8].sum(axis=0)),\n", + " ):\n", + " ax.plot(scaled_times, mean, color=color, linewidth=1.8, label=label)\n", + " if samples.shape[0] > 1:\n", + " standard_error = samples.std(axis=0, ddof=1) / np.sqrt(samples.shape[0])\n", + " ax.fill_between(scaled_times, mean - standard_error, mean + standard_error, color=color, alpha=0.15)\n", + "axes[2, 0].set(ylabel=r\"Right transmon $|1\\rangle$ population\")\n", + "axes[2, 1].set(ylabel=r\"Total excitation $\\sum_i\\langle n_i\\rangle$\")\n", + "for ax, title in zip(axes[2], [\"(e) Received excitation\", \"(f) Excitation remaining\"], strict=True):\n", + " ax.set(xlabel=r\"Time $t/T$\", xlim=(0, 1), ylim=(0, 1.08))\n", + " ax.set_title(title, loc=\"left\", fontsize=11)\n", + "axes[2, 0].legend(fontsize=8, loc=\"upper left\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "**Stronger noise suppresses transfer to the right transmon.** Relaxation removes\n", + "excitation, while dephasing disrupts the coherent exchange through the\n", + "resonator. The lower panels separate received population from total excitation;\n", + "the latter can have small coherent excursions because of the counter-rotating\n", + "terms. All heatmaps use one color scale. Shading shows one standard error from\n", + "the trajectory samples, with sites summed within each trajectory before\n", + "estimating uncertainty in total excitation. These bands measure sampling\n", + "uncertainty, not numerical or level-cutoff error.\n", + "\n", + "## Further options\n", + "\n", + "Increase `num_traj` to reduce sampling fluctuations and refine `dt` and\n", + "numerical tolerances to check propagation accuracy. Increase `qubit_dim` and\n", + "`resonator_dim` separately to check level truncation, rebuilding the state,\n", + "observables, and jump matrices to match. Three transmon levels suffice to\n", + "illustrate leakage here; they do not establish convergence for a driven device.\n", + "\n", + "To add photon loss, supply a resonator-sized annihilation matrix at site 1.\n", + "Other custom channels and distributed strengths are described in\n", + "{doc}`realistic_noise_models`. See {doc}`hamiltonians` for longer alternating\n", + "chains, {doc}`state_initialization` for other preparations, and\n", + "{doc}`simulation_parameters` for accuracy and trajectory settings." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 34, + 53, + 72, + 78, + 90, + 111, + 129, + 137, + 145, + 172, + 189, + 204, + 218, + 252 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/examples/trapped_ion.ipynb b/docs/_outputs/examples/trapped_ion.ipynb new file mode 100644 index 000000000..b9eba7e23 --- /dev/null +++ b/docs/_outputs/examples/trapped_ion.ipynb @@ -0,0 +1,4058 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-0", + "metadata": {}, + "source": [ + "# Trapped Ion Emulation\n", + "\n", + "Moving a trapped ion changes its position, but can also leave it oscillating\n", + "after the trap stops. Random force impulses add another source of motion. We\n", + "first follow a displaced wavepacket in a fixed harmonic well, then move the well\n", + "and compare several noise strengths. Position distributions and motional energy\n", + "show how coherent transport excitation differs from heating.\n", + "\n", + "This guide uses the standard YAQS installation and Matplotlib. Run the cells in\n", + "order in a notebook. For a script, use the entry-point guard in\n", + "{doc}`simulator_initialization`.\n", + "\n", + "## 1. Build a harmonic trap on a position grid\n", + "\n", + "Each ion occupies one MPO site, whose local basis consists of position-grid\n", + "points. `MPO.trapped_ion` combines a finite-difference kinetic operator with a\n", + "harmonic potential centered at `trap_center`. We use one ion and dimensionless\n", + "units with $\\hbar=m=\\omega=1$. Position is in oscillator-length units and time\n", + "is in units of $1/\\omega$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from mqt.yaqs import Hamiltonian, MPO, State\n", + "\n", + "positions = np.linspace(-8.0, 8.0, 65)\n", + "grid_dim = len(positions)\n", + "grid_spacing = positions[1] - positions[0]\n", + "omega = 1.0\n", + "\n", + "\n", + "def trap_at(center):\n", + " return Hamiltonian.from_mpo(\n", + " MPO.trapped_ion(positions, masses=[1.0], omega=omega, trap_center=center)\n", + " )\n", + "\n", + "\n", + "def state_at(center):\n", + " packet = np.exp(-0.5 * (positions - center) ** 2).astype(complex)\n", + " packet /= np.linalg.norm(packet)\n", + " return State(\n", + " 1, tensors=[packet.reshape(grid_dim, 1, 1)], physical_dimensions=[grid_dim],\n", + " )\n", + "\n", + "\n", + "static_hamiltonian = trap_at(0.0)\n", + "static_state = state_at(1.0)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-2", + "metadata": {}, + "source": [ + "The normalized Gaussian approximates a displaced oscillator ground state. Its\n", + "components are amplitudes on the finite grid, so their squared magnitudes sum to\n", + "one. A single MPS tensor has shape `(grid_dim, 1, 1)`: one physical index and\n", + "two bond indices. This representation supports both the fixed and moving\n", + "Hamiltonians below. Supplying `vector=` instead selects the MCWF backend, which\n", + "does not support piecewise Hamiltonians.\n", + "\n", + "## 2. Follow an oscillating wavepacket\n", + "\n", + "Measure the mean position and the population of every grid point. The latter\n", + "uses projectors $|x_j\\rangle\\langle x_j|$ and gives a position distribution at\n", + "each sampled time, without requesting a sequence of output states." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-3", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import AnalogSimParams, Observable, Simulator\n", + "\n", + "position_observable = Observable(\"position\", 0, positions=positions)\n", + "grid_projectors = [Observable(np.diag(row), 0) for row in np.eye(grid_dim)]\n", + "period = 2 * np.pi / omega\n", + "static_params = AnalogSimParams(\n", + " observables=[position_observable, *grid_projectors],\n", + " elapsed_time=period,\n", + " dt=period / 80,\n", + " preset=\"balanced\",\n", + ")\n", + "simulator = Simulator(show_progress=False)\n", + "static_result = simulator.run(static_state, static_hamiltonian, static_params)\n", + "static_times = static_result.times\n", + "static_values = np.asarray(static_result.expectation_values)\n", + "static_density = static_values[1:] / grid_spacing" + ] + }, + { + "cell_type": "markdown", + "id": "cell-4", + "metadata": {}, + "source": [ + "`expectation_values` follows the supplied observable order. Stacking these\n", + "arrays gives shape `(66, 81)`: the mean position, then 65 grid populations.\n", + "Divide populations by the grid spacing to plot probability density per unit\n", + "position. In the continuum, the mean follows $\\langle x(t)\\rangle=\\cos t$. That\n", + "curve is a useful comparison; the finite-difference Hamiltonian differs slightly\n", + "from the continuum oscillator." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-5", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:40:22.505079\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " P\n", + " o\n", + " s\n", + " i\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " x\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) Position distribution and mean\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " π\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " M\n", + " e\n", + " a\n", + " n\n", + "  \n", + " p\n", + " o\n", + " s\n", + " i\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " ⟨\n", + " ⟩\n", + " x\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (b) Oscillation in a fixed well\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " YAQS\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Continuum\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " Probability density\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from matplotlib_inline.backend_inline import set_matplotlib_formats\n", + "\n", + "set_matplotlib_formats(\"svg\")\n", + "plt.rcParams.update({\n", + " \"font.family\": \"serif\", \"font.serif\": [\"STIXGeneral\"], \"mathtext.fontset\": \"stix\",\n", + " \"font.size\": 10, \"axes.labelsize\": 11, \"axes.linewidth\": 0.8,\n", + " \"xtick.direction\": \"in\", \"ytick.direction\": \"in\", \"svg.fonttype\": \"none\",\n", + " \"legend.frameon\": False,\n", + "})\n", + "fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9), layout=\"constrained\")\n", + "image = axes[0].pcolormesh(\n", + " static_times, positions, static_density, shading=\"auto\", cmap=\"cividis\",\n", + " vmin=0, vmax=0.6, rasterized=True,\n", + ")\n", + "axes[0].plot(static_times, static_values[0], color=\"white\", linewidth=1.3)\n", + "axes[0].set(ylabel=r\"Position $x$\", ylim=(-3, 3))\n", + "axes[0].set_title(\"(a) Position distribution and mean\", loc=\"left\", fontsize=11)\n", + "fig.colorbar(image, ax=axes[0], label=\"Probability density\", shrink=0.85)\n", + "axes[1].plot(static_times, static_values[0], color=\"#0072B2\", linewidth=1.8, label=\"YAQS\")\n", + "axes[1].plot(static_times, np.cos(static_times), color=\"0.4\", linestyle=\"--\", label=\"Continuum\")\n", + "axes[1].set(ylabel=r\"Mean position $\\langle x\\rangle$\", ylim=(-1.15, 1.15))\n", + "axes[1].set_title(\"(b) Oscillation in a fixed well\", loc=\"left\", fontsize=11)\n", + "axes[1].legend(fontsize=9)\n", + "for ax in axes:\n", + " ax.set(xlabel=r\"Time $t$\", xlim=(0, period), xticks=[0, np.pi, 2 * np.pi],\n", + " xticklabels=[\"0\", r\"$\\pi$\", r\"$2\\pi$\"])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-6", + "metadata": {}, + "source": [ + "**The packet oscillates about the trap center.** Its mean nearly follows the\n", + "continuum curve over one period. The heatmap contains the actual YAQS grid\n", + "populations; the white line marks their mean. Small changes in shape and phase\n", + "reflect the finite grid and its kinetic operator.\n", + "\n", + "## 3. Move the well, then hold it fixed\n", + "\n", + "Start a new packet at $q=-1$ and translate the well to $q=1$ over four time\n", + "units. The control is a staircase: each trap center remains fixed for `dt=0.1`,\n", + "followed by a four-unit hold at the target. This deliberately simple protocol\n", + "leaves enough residual motion to see in the position distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-7", + "metadata": {}, + "outputs": [], + "source": [ + "start_center = -1.0\n", + "target_center = 1.0\n", + "transport_duration = 4.0\n", + "hold_duration = 4.0\n", + "dt = 0.1\n", + "n_transport = round(transport_duration / dt)\n", + "transport_centers = np.linspace(start_center, target_center, n_transport, endpoint=False)\n", + "target_hamiltonian = trap_at(target_center)\n", + "moving_hamiltonian = Hamiltonian.piecewise([\n", + " *[(trap_at(center), dt) for center in transport_centers],\n", + " (target_hamiltonian, hold_duration),\n", + "])\n", + "transport_state = state_at(start_center)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-8", + "metadata": {}, + "source": [ + "`Hamiltonian.piecewise` selects the well for each interval. Piece durations must\n", + "be integer multiples of the simulation timestep, and their sum must equal\n", + "`elapsed_time`. Piecewise evolution currently requires an MPS and TDVP, which\n", + "are used here. Reducing `dt` while rebuilding the staircase also changes the\n", + "control waveform; it is a separate check from refining the spatial grid.\n", + "\n", + "## 4. Measure residual motion and heating\n", + "\n", + "Alongside the mean and grid populations, measure $\\langle x^2\\rangle$ and the\n", + "energy of the final well. The ensemble position width is\n", + "$\\sigma_x=\\sqrt{\\langle x^2\\rangle-\\langle x\\rangle^2}$. During the hold, the\n", + "final-well energy measures motion left by transport and added by noise." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-9", + "metadata": {}, + "outputs": [], + "source": [ + "transport_params = AnalogSimParams(\n", + " observables=[\n", + " position_observable,\n", + " Observable(np.diag(positions**2), 0),\n", + " Observable(target_hamiltonian.to_matrix(), 0),\n", + " *grid_projectors,\n", + " ],\n", + " elapsed_time=transport_duration + hold_duration,\n", + " dt=dt,\n", + " order=2,\n", + " num_traj=32,\n", + " preset=\"balanced\",\n", + " random_seed=7,\n", + ")\n", + "noiseless = simulator.run(transport_state, moving_hamiltonian, transport_params)\n", + "times = noiseless.times" + ] + }, + { + "cell_type": "markdown", + "id": "cell-10", + "metadata": {}, + "source": [ + "The observable arrays have shape `(68, 81)`: mean position, mean squared\n", + "position, final-well energy, then grid populations. The `balanced` preset sets\n", + "numerical tolerances. Second-order TJM averages 32 trajectories for each noisy\n", + "run below; the noiseless baseline needs only one trajectory.\n", + "\n", + "## 5. Add random momentum kicks\n", + "\n", + "Model random force impulses as momentum kicks in either direction. Multiplying\n", + "the wavefunction by $e^{\\pm i\\kappa x}$ shifts its momentum by $\\pm\\kappa$ in\n", + "our units. The two custom jumps are $L_\\pm=\\sqrt{\\gamma/2}\\,e^{\\pm i\\kappa X}$,\n", + "where $X=\\operatorname{diag}(x_j)$ and $\\kappa=1$. Equal rates give no preferred\n", + "direction. The kicks heat the ion without friction or thermal relaxation." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cell-11", + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.yaqs import NoiseModel\n", + "\n", + "noise_rates = [0.1, 0.4, 1.0]\n", + "kick_size = 1.0\n", + "results = {0.0: noiseless}\n", + "for rate in noise_rates:\n", + " noise = NoiseModel([\n", + " {\"name\": \"momentum_kick\", \"sites\": [0], \"strength\": rate / 2,\n", + " \"matrix\": np.diag(np.exp(1j * sign * kick_size * positions))}\n", + " for sign in (-1, 1)\n", + " ])\n", + " results[rate] = simulator.run(transport_state, moving_hamiltonian, transport_params, noise)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "YAQS multiplies each supplied matrix by `sqrt(strength)`. Each direction has\n", + "rate $\\gamma/2$, so $\\gamma$ is the total kick rate in oscillator units. The\n", + "noise acts throughout transport and the hold, while the initial state, control\n", + "waveform, time grid, and numerical settings stay fixed. The stronger rates make\n", + "spreading visible over this short protocol. The grid extends to $x=\\pm8$ to\n", + "leave room for the heated packet.\n", + "\n", + "Parallel execution is enabled by default. The documentation suppresses progress\n", + "bars with `show_progress=False`; omit that setting to see progress. The seed\n", + "fixes random streams for the same configuration, and each run preserves the\n", + "input state." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cell-13", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2026-10-10T22:40:58.370729\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.11.2, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " P\n", + " o\n", + " s\n", + " i\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " x\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (a) No noise\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Trap center\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Mean position\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " P\n", + " o\n", + " s\n", + " i\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " x\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (\n", + " b\n", + " )\n", + "  \n", + " =\n", + " 0\n", + " .\n", + " 1\n", + " γ\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " P\n", + " o\n", + " s\n", + " i\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " x\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (\n", + " c\n", + " )\n", + "  \n", + " =\n", + " 0\n", + " .\n", + " 4\n", + " γ\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " −5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " P\n", + " o\n", + " s\n", + " i\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " x\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (\n", + " d\n", + " )\n", + "  \n", + " =\n", + " 1\n", + " γ\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.75\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.25\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.50\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1.75\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2.00\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " P\n", + " o\n", + " s\n", + " i\n", + " t\n", + " i\n", + " o\n", + " n\n", + "  \n", + " w\n", + " i\n", + " d\n", + " t\n", + " h\n", + "  \n", + " σ\n", + " x\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (e) Ensemble position spread\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " No noise\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " γ\n", + " =\n", + " 0\n", + " .\n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " γ\n", + " =\n", + " 0\n", + " .\n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " γ\n", + " =\n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 6\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 7\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 8\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " T\n", + " i\n", + " m\n", + " e\n", + "  \n", + " t\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " E\n", + " n\n", + " e\n", + " r\n", + " g\n", + " y\n", + "  \n", + " ⟨\n", + " (\n", + " =\n", + " 1\n", + " )\n", + " ⟩\n", + " H\n", + " q\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " (f) Motional energy during the hold\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.3\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.4\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.5\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0.6\n", + " \n", + " \n", + " \n", + " Probability density\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib.colors import Normalize\n", + "\n", + "colors = [\"0.2\", \"#56B4E9\", \"#0072B2\", \"#D55E00\"]\n", + "n_hold = round(hold_duration / dt)\n", + "scheduled_centers = np.r_[transport_centers, np.full(n_hold + 1, target_center)]\n", + "hold_mask = times >= transport_duration\n", + "fig, axes = plt.subplots(3, 2, figsize=(7.2, 7.1), layout=\"constrained\")\n", + "for index, (ax, (rate, result)) in enumerate(zip(axes[:2].flat, results.items(), strict=True)):\n", + " means = np.asarray(result.expectation_values)\n", + " density = means[3:] / grid_spacing\n", + " image = ax.pcolormesh(times, positions, density, shading=\"auto\", cmap=\"cividis\",\n", + " norm=Normalize(0, 0.6), rasterized=True)\n", + " ax.step(times, scheduled_centers, where=\"post\", color=\"white\", linestyle=\"--\",\n", + " linewidth=1.1, label=\"Trap center\")\n", + " ax.plot(times, means[0], color=\"#E69F00\", linewidth=1.2, label=\"Mean position\")\n", + " title = \"(a) No noise\" if index == 0 else rf\"({chr(97 + index)}) $\\gamma={rate:g}$\"\n", + " ax.set(xlabel=r\"Time $t$\", ylabel=r\"Position $x$\", xlim=(0, times[-1]), ylim=(-8, 8))\n", + " ax.set_title(title, loc=\"left\", fontsize=11)\n", + " if index == 0:\n", + " ax.legend(loc=\"upper left\", fontsize=8, labelcolor=\"white\")\n", + "fig.colorbar(image, ax=list(axes[:2].flat), label=\"Probability density\", shrink=0.8)\n", + "\n", + "for (rate, result), color in zip(results.items(), colors, strict=True):\n", + " means = np.asarray(result.expectation_values)\n", + " samples = result.trajectories[2]\n", + " energy_se = samples.std(axis=0, ddof=1) / np.sqrt(len(samples)) if len(samples) > 1 else np.zeros_like(times)\n", + " width = np.sqrt(means[1] - means[0] ** 2)\n", + " label = \"No noise\" if rate == 0 else rf\"$\\gamma={rate:g}$\"\n", + " axes[2, 0].plot(times, width, color=color, linewidth=1.7, label=label)\n", + " axes[2, 1].plot(times[hold_mask], means[2, hold_mask], color=color, linewidth=1.7)\n", + " axes[2, 1].fill_between(times[hold_mask], (means[2] - energy_se)[hold_mask],\n", + " (means[2] + energy_se)[hold_mask], color=color, alpha=0.15, linewidth=0)\n", + "axes[2, 0].axvline(transport_duration, color=\"0.5\", linestyle=\":\", linewidth=1)\n", + "axes[2, 0].set(xlabel=r\"Time $t$\", ylabel=r\"Position width $\\sigma_x$\", xlim=(0, times[-1]))\n", + "axes[2, 0].set_title(\"(e) Ensemble position spread\", loc=\"left\", fontsize=11)\n", + "axes[2, 0].legend(fontsize=8, ncol=2)\n", + "axes[2, 1].set(xlabel=r\"Time $t$\", ylabel=r\"Energy $\\langle H(q=1)\\rangle$\",\n", + " xlim=(transport_duration, times[-1]))\n", + "axes[2, 1].set_title(\"(f) Motional energy during the hold\", loc=\"left\", fontsize=11)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-14", + "metadata": {}, + "source": [ + "**Transport leaves a coherent oscillation; random kicks broaden the packet and\n", + "raises its energy.** After $t=4$, the dashed trap center stays fixed while the\n", + "mean position continues to oscillate. Without noise, the packet remains narrow\n", + "and its energy stays constant during the hold. Stronger noise spreads the\n", + "position distribution and increases the motional energy. All heatmaps share one\n", + "color scale.\n", + "\n", + "The width in panel (e) describes the ensemble position distribution, including\n", + "variation between trajectories. It is not an error bar on the mean position.\n", + "Shading in panel (f) shows one standard error of the trajectory-averaged energy.\n", + "Thirty-two trajectories make the trend visible, but the curves retain sampling\n", + "fluctuations. Increase `num_traj` to resolve smaller differences.\n", + "\n", + "## 6. Adapt the model\n", + "\n", + "Check grid spacing and boundaries before interpreting a quantitative result. The\n", + "kinetic operator uses zero exterior boundary values, and a heated packet can\n", + "reach the edges. Refine the grid and enlarge its range separately. The continuum\n", + "Gaussian is also only an approximate ground state of the discrete Hamiltonian.\n", + "For dimensional inputs, pass `hbar` to `MPO.trapped_ion` in units consistent\n", + "with the masses, positions, and angular frequency. The kinetic term depends on\n", + "$\\hbar^2$. For time in seconds, divide one MPO core by $\\hbar$ with\n", + "`mpo.tensors[0] /= hbar` before wrapping it with `Hamiltonian.from_mpo`. This\n", + "converts the energy operator to $H/\\hbar$ for YAQS's evolution convention\n", + "$\\exp(-iH\\,dt)$.\n", + "\n", + "A slower or smoother transport protocol can reduce coherent residual motion. The\n", + "kick model isolates heating from random impulses. Other noise processes require\n", + "suitable jump operators; this model does not describe cooling. The factory\n", + "supports one or two ions, with a softened Coulomb interaction for two ions. It\n", + "describes motional dynamics on a position grid, rather than internal spin states\n", + "or a full laser-driven gate model.\n", + "\n", + "For a noiseless run, `get_state=True` also returns the final state. Noisy MPS\n", + "runs return ensemble observables and trajectory data instead of a single final\n", + "pure state. Grid projectors remain available for both cases, as shown above.\n", + "\n", + "## Related guides\n", + "\n", + "- {doc}`hamiltonians` — trap parameters, two-ion interactions, and piecewise\n", + " models.\n", + "- {doc}`analog_simulation` — noisy evolution, accuracy, and trajectory sampling.\n", + "- {doc}`state_initialization` — custom local dimensions and manual MPS tensors.\n", + "- {doc}`transmon_emulation` — excitation transfer in a multilevel hardware\n", + " model." + ] + } + ], + "metadata": { + "file_format": "mystnb", + "kernelspec": { + "display_name": "python3", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + }, + "mystnb": { + "execution_timeout": 300, + "number_source_lines": true + }, + "source_map": [ + 10, + 32, + 59, + 74, + 91, + 100, + 130, + 144, + 158, + 173, + 190, + 205, + 218, + 232, + 274 + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/_outputs/manifest.json b/docs/_outputs/manifest.json new file mode 100644 index 000000000..eeafcfea2 --- /dev/null +++ b/docs/_outputs/manifest.json @@ -0,0 +1,85 @@ +{ + "runtime": "26a68889dee2dd035426197b92930edf467dc2231aaf0617087d9a60a95a894b", + "environment": { + "python": "3.14.2", + "mqt.yaqs": "0.6.1.dev306+gd3f16427f.d20261010", + "numpy": "2.5.4", + "scipy": "1.18.1", + "qiskit": "2.5.2", + "myst-nb": "1.4.0" + }, + "notebooks": { + "examples/analog_simulation.md": { + "inputs": "82e212babe360ab5e463ec8df7e4cf20fd50ed1e332280d85f1ae3da77782f2a", + "sha256": "da7932f0aa5ad7ddbb88c6c202a14913458d4c2c30393fa5cb76018d950edf6d" + }, + "examples/characterization.md": { + "inputs": "76a808e463c921cd3f2615d718ab8a4eee344cb79f379919cd2b7d08bb4702a3", + "sha256": "c4c892043be034706de59be6342fe4f20a4f0b0838184cd331e26305d7583eb2" + }, + "examples/circuit_observables.md": { + "inputs": "001b16f304bebc81a638bcf7f8825782b2635fe99567e2ccc78d5abd54f462e3", + "sha256": "ccb255261f53b9ef06b7a8ea29ca34a46451e19775d29be4738dcf473f517ed5" + }, + "examples/circuit_shots.md": { + "inputs": "c70da7ba94370119b025bd136ef95dfd969804f2daad264b6d43db279bf22211", + "sha256": "bafba85bc2805db14ff545fa9bbd94371ebd5dfb611656eafc92c46d34e1c016" + }, + "examples/digital_analog_simulation.md": { + "inputs": "e50dae39c76469ecf9200247f527e75c3b61a47ceff3b84f55f94ead817a7b4a", + "sha256": "381ad238d82cc4633974f40fa86339399109fe5cb34c64a92fd328c4e57f98b3" + }, + "examples/digital_twin.md": { + "inputs": "15b9a88f309282017c5f4778262cd10ea4dbe578d81ff237447e8ebae2bd002a", + "sha256": "1562cee35f913fc910e0e5210b1f3213b2ea123a9ada6ce3f8bd7cbf8ac69f72" + }, + "examples/ensemble_evolution.md": { + "inputs": "7248268da7bafd0bd135dbcc7dba71e3a5f445a813759f4e9211c75104a0891b", + "sha256": "c44717678def06e333bbfe61cdbcdb5680d402931563a2d0b7efee5f4473cf78" + }, + "examples/equivalence_checking.md": { + "inputs": "2699f078a801ab9cc39fc02a2918c5f19a1a8c310b501e587f743972d980f6dd", + "sha256": "4fbd529ced5be38d33cccb5f1384af6778d2aa0f51c1588b7a376c25d00f41bc" + }, + "examples/hamiltonians.md": { + "inputs": "b62224074d85fb4e0c277e52e379c1a083b2611ea45b0da0aea6bd41ab9ad8d4", + "sha256": "e7acd992c5ae7b6569bea733b774b07f81a81904c6cde3557b64eb83009f26ea" + }, + "examples/memory_surrogate.md": { + "inputs": "24601f1e39b1f4615c947c5bbd1e3329b3d2fff76f94969c3d85d8a87352d98f", + "sha256": "003a5c27c843f7e1a744307652e782e138910f153724f02cbba03f047eef37bc" + }, + "examples/quickstart.md": { + "inputs": "13df95d9944015b3b5ab87a820c58b35897b572c0497cd444f05f8c209bca245", + "sha256": "2c0ddd9d86ed51264dbee2d278d5456176f5ff4daa75872dc1c8d7d2e9c7ed46" + }, + "examples/realistic_noise_models.md": { + "inputs": "25f2cb4f20dc93446b340828c1f481fd36a933af2f69a3a0fbaebfcd28f3475f", + "sha256": "0665c26d5c20dfb16f9fb36efae5af8fd7a06d9c240d005b7c738d2804376c83" + }, + "examples/representation_comparison.md": { + "inputs": "98be45e8dbcd5f8031598e4f1da4fb36b8269cd0575b177fed84511577875b93", + "sha256": "f982b824f982a4177e873565aa8ac629d765b076d50540feb310f2587300eac8" + }, + "examples/simulation_parameters.md": { + "inputs": "491837e1260626ff1bc5bac53076e8aaf801a1ef46f7032b7d49a9185f557b5e", + "sha256": "fe21a22f1d07ec1e53ea98ef0c52966160c2ae40e4059629a591894c9b8c18ad" + }, + "examples/simulator_initialization.md": { + "inputs": "633cdce6caca17f471a25cd6874451d6d3c52149bf81e3328900e6955fa3ef6e", + "sha256": "2f4788b60ac63e2ec7910cc0b76c33fc0422d92fb6f5b1e1b93f9663c83d1665" + }, + "examples/state_initialization.md": { + "inputs": "c84de05e36fc2d4de745881e6c4a9342060c02c4a6bee91bba9e8653ea0bfec4", + "sha256": "a188d39df83150e4e94d7930a870ee2f1d25375a4a286c9ff0cb1879a6c2cb76" + }, + "examples/transmon_emulation.md": { + "inputs": "17f7c82a1b786008e00bbf555a30e87b2b38c1201e3489a5a9a8d7ba6b189ddd", + "sha256": "4cd86d9a562ad23851fbff0078d68e21895ed728a611762b49345bd95a82960f" + }, + "examples/trapped_ion.md": { + "inputs": "2669cd14c6e706b2dd5a44de291e78d77d457d41522afc662a6a4db694fa8d3a", + "sha256": "9c36e491e5b247c4520ba59c7a9f1e9c91dbfbd9b185a7cfc22ceac578c12b1d" + } + } +} diff --git a/docs/conf.py b/docs/conf.py index 9c6ed9d13..2748cdccc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,6 +76,7 @@ "sphinxcontrib.bibtex", "sphinxext.opengraph", "yaqs_api", + "yaqs_examples", ] source_suffix = [".rst", ".md"] @@ -83,6 +84,7 @@ exclude_patterns = [ "_build", + "_outputs", "**.ipynb_checkpoints", "**.jupyter_cache", "**jupyter_execute", @@ -124,7 +126,7 @@ nb_execution_mode = "cache" nb_execution_raise_on_error = True -nb_execution_cache_path = str(ROOT / "docs" / "_build" / ".jupyter_cache") +nb_execution_cache_path = os.environ.get("YAQS_DOCS_CACHE", str(ROOT / "docs" / "_build" / ".jupyter_cache")) # MyST-NB does not know sphinx-llm's builder name. Preserve figures instead of # selecting only their text representation in the generated Markdown. nb_mime_priority_overrides = [ diff --git a/noxfile.py b/noxfile.py index 06beb198e..962316d30 100755 --- a/noxfile.py +++ b/noxfile.py @@ -245,7 +245,7 @@ def docs(session: nox.Session) -> None: args, posargs = parser.parse_known_args(session.posargs) serve = args.builder == "html" and session.interactive - install_args = ["--group", "docs", "--torch-backend", "cpu", "--exact", "-e", ".[qasm3,torch]"] + install_args = ["--group", "docs", "--exact", "-e", "."] if serve: install_args.append("sphinx-autobuild") session.install(*install_args) @@ -268,10 +268,38 @@ def docs(session: nox.Session) -> None: ) +@nox.session(name="docs-execute", python="3.14", reuse_venv=True) +def docs_execute(session: nox.Session) -> None: + """Execute all examples and regenerate saved notebook outputs.""" + session.install("--group", "docs", "--torch-backend", "cpu", "--exact", "-e", ".[qasm3,torch]") + with tempfile.TemporaryDirectory(prefix="yaqs-docs-execution-") as cache: + session.run( + "sphinx-build", + "-E", + "-a", + "-n", + "-T", + "-W", + "--keep-going", + "-j", + "1", + "-d", + "docs/_build/executed-doctrees", + "docs", + "docs/_build/executed", + env={ + **_CAPPED_NUMERICAL_THREADS, + "YAQS_MAX_WORKERS": "3", + "YAQS_DOCS_EXECUTE": "1", + "YAQS_DOCS_CACHE": cache, + }, + ) + + @nox.session(name="docs-check", python="3.14", reuse_venv=True) def docs_check(session: nox.Session) -> None: """Test the build settings and check references without running guide notebooks.""" - session.install("--group", "docs", "--group", "test", "--torch-backend", "cpu", "--exact", "-e", ".[qasm3,torch]") + session.install("--group", "docs", "--group", "test", "--exact", "-e", ".") session.run("pytest", "-n", "0", "tests/docs", env=_CAPPED_NUMERICAL_THREADS) session.run( "sphinx-build", @@ -282,8 +310,6 @@ def docs_check(session: nox.Session) -> None: "-W", "--keep-going", "-D", - "nb_execution_mode=off", - "-D", "llms_txt_enabled=0", "docs", "docs/_build/check", diff --git a/pyproject.toml b/pyproject.toml index 57e96e4f1..80d6ad40f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,9 @@ test = [ ] docs = [ "furo>=2025.12.19", + "jupyter-cache>=1", "myst-nb>=1.4", + "nbformat>=5.10", "qiskit[visualization]>=2.1,<3", "sphinx>=9", "sphinx-autoapi>=3.6", diff --git a/tests/docs/test_build.py b/tests/docs/test_build.py index 0c3370ca5..655889099 100644 --- a/tests/docs/test_build.py +++ b/tests/docs/test_build.py @@ -93,7 +93,7 @@ def test_included_release_links(tmp_path: Path, *, missing_source: bool) -> None def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: - """Both documentation formats retain outputs from one capped notebook run.""" + """Publishing restores both formats from saved outputs without starting a kernel.""" pytest.importorskip("sphinx") pytest.importorskip("myst_nb") pytest.importorskip("sphinx_llm.txt") @@ -102,7 +102,7 @@ def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: source = tmp_path / "docs" source.mkdir() - _write_configuration(source, ["myst_nb", "sphinx_llm.txt", "yaqs_api"]) + _write_configuration(source, ["myst_nb", "sphinx_llm.txt", "yaqs_api", "yaqs_examples"]) records = tmp_path / "executions.jsonl" (source / "index.md").write_text( "---\nfile_format: mystnb\nkernelspec:\n name: python3\nlanguage_info:\n name: python\n---\n\n" @@ -149,8 +149,30 @@ def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: environment["JUPYTER_RUNTIME_DIR"] = str(tmp_path / ".jupyter") environment["READTHEDOCS_VIRTUALENV_PATH"] = sys.prefix environment["READTHEDOCS_OUTPUT"] = str(output.parent) + environment["YAQS_DOCS_CACHE"] = str(tmp_path / "notebook-cache") configuration = yaml.safe_load((Path(__file__).parents[2] / ".readthedocs.yaml").read_text()) command = configuration["build"]["jobs"]["build"]["html"][0] + generated = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - sys.executable is trusted. + [Template(argument).substitute(environment) for argument in shlex.split(command)], + cwd=tmp_path, + env={**environment, "YAQS_DOCS_EXECUTE": "1"}, + check=False, + capture_output=True, + text=True, + timeout=60, + ) + assert generated.returncode == 0, generated.stdout + generated.stderr + assert (source / "_outputs" / "index.ipynb").is_file() + assert (source / "_outputs" / "manifest.json").is_file() + shutil.rmtree(output.parent) + shutil.rmtree(source / "_build") + shutil.rmtree(environment["YAQS_DOCS_CACHE"]) + # Prose changes must appear beside the saved outputs without invalidating them. + index = source / "index.md" + index.write_text( + index.read_text().replace("# Executable documentation", "# Updated documentation\n\nRevised guide text.") + ) + environment.pop("YAQS_DOCS_EXECUTE", None) completed = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - sys.executable is trusted. [Template(argument).substitute(environment) for argument in shlex.split(command)], cwd=tmp_path, @@ -180,6 +202,49 @@ def test_html_and_markdown_share_notebook_execution(tmp_path: Path) -> None: assert (output / figures[0]).is_file() assert figures[0] in (output / "index.html").read_text() assert "image/svg+xml" not in markdown + assert "Updated documentation" in (output / "index.html").read_text() + + package = tmp_path / "src" / "example.py" + package.parent.mkdir() + saved = source / "_outputs" / "index.ipynb" + original_source = index.read_text() + original_output = saved.read_text() + invalid_inputs = ( + (index, original_source.replace("6 * 7", "6 * 8"), "stale example outputs"), + ( + index, + original_source.replace("language_info:", "mystnb:\n execution_mode: force\nlanguage_info:"), + "requires", + ), + (package, "changed = True\n", "package code or dependencies"), + (tmp_path / "uv.lock", "changed dependencies\n", "package code or dependencies"), + (source / "_outputs" / "manifest.json", None, "Missing or invalid example output manifest"), + (saved, None, "Missing or changed saved notebook"), + (saved, original_output + "\n", "Missing or changed saved notebook"), + (source / "conf.py", (source / "conf.py").read_text() + '\nnb_execution_mode = "force"\n', "requires"), + ) + for path, replacement, message in invalid_inputs: + previous = path.read_text() if path.exists() else None + if replacement is None: + path.unlink() + else: + path.write_text(replacement) + rejected = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - sys.executable is trusted. + [sys.executable, "-m", "sphinx", "-E", "-W", "-b", "html", str(source), str(output)], + cwd=tmp_path, + env=environment, + check=False, + capture_output=True, + text=True, + timeout=60, + ) + assert rejected.returncode != 0, str(path) + assert message in rejected.stdout + rejected.stderr + assert len(records.read_text().splitlines()) == 1 + if previous is None: + path.unlink() + else: + path.write_text(previous) @pytest.mark.parametrize("missing_reference", [False, True]) diff --git a/uv.lock b/uv.lock index 5990c0bcc..359ee134d 100644 --- a/uv.lock +++ b/uv.lock @@ -3,10 +3,10 @@ revision = 5 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version >= '3.15' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'emscripten'", "(python_full_version >= '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "(python_full_version == '3.14.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", @@ -521,10 +521,10 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version >= '3.15' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'emscripten'", "(python_full_version >= '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "(python_full_version == '3.14.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", @@ -1685,7 +1685,9 @@ dev = [ ] docs = [ { name = "furo" }, + { name = "jupyter-cache" }, { name = "myst-nb" }, + { name = "nbformat" }, { name = "qiskit", extra = ["visualization"] }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, @@ -1736,7 +1738,9 @@ dev = [ ] docs = [ { name = "furo", specifier = ">=2025.12.19" }, + { name = "jupyter-cache", specifier = ">=1" }, { name = "myst-nb", specifier = ">=1.4" }, + { name = "nbformat", specifier = ">=5.10" }, { name = "qiskit", extras = ["visualization"], specifier = ">=2.1,<3" }, { name = "sphinx", specifier = ">=9" }, { name = "sphinx-autoapi", specifier = ">=3.6" }, @@ -1853,10 +1857,10 @@ version = "3.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version >= '3.15' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'emscripten'", "(python_full_version >= '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "(python_full_version == '3.14.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", @@ -2025,10 +2029,10 @@ version = "2.5.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version >= '3.15' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'emscripten'", "(python_full_version >= '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "(python_full_version == '3.14.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", @@ -3159,10 +3163,10 @@ version = "1.18.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version >= '3.15' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'emscripten'", "(python_full_version >= '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "(python_full_version == '3.14.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", @@ -3333,10 +3337,10 @@ version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version >= '3.15' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'emscripten'", "(python_full_version >= '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "(python_full_version == '3.14.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')",