diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css
index 7e1083f..911c601 100644
--- a/docs/source/_static/custom.css
+++ b/docs/source/_static/custom.css
@@ -39,3 +39,11 @@ a.gh-link:visited {
.sphx-glr-script-out pre {
padding: 0;
}
+
+.sphx-glr-thumbnails:empty {
+ display: none;
+}
+
+.sphx-glr-tag-list:empty {
+ display: none;
+}
\ No newline at end of file
diff --git a/docs/source/_static/tune_correction.svg b/docs/source/_static/tune_correction.svg
new file mode 100644
index 0000000..f98e255
--- /dev/null
+++ b/docs/source/_static/tune_correction.svg
@@ -0,0 +1,92 @@
+
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 32aa08f..8cd6f7a 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -56,10 +56,10 @@
}
exclude_patterns = [
- "tutorials/*.py",
- "tutorials/*.ipynb",
- "tutorials/*.zip",
- "tutorials/*.codeobj.json",
+ "tutorials/**/*.py",
+ "tutorials/**/*.ipynb",
+ "tutorials/**/*.zip",
+ "tutorials/**/*.codeobj.json",
]
# -- Options for HTML output -------------------------------------------------
diff --git a/docs/tutorials/GALLERY_HEADER.rst b/docs/tutorials/GALLERY_HEADER.rst
index 919cc16..eef415e 100644
--- a/docs/tutorials/GALLERY_HEADER.rst
+++ b/docs/tutorials/GALLERY_HEADER.rst
@@ -3,5 +3,7 @@ Tutorials
Here you can find learning-oriented tutorials that guide you through the functionality of pyAML step by step.
-The tutorials are designed to be followed in sequence, gradually increasing in complexity but
-each tutorial is self-contained and can also be completed independently.
+The notebooks can be run using `Binder `_ or be downloaded
+and run on your own computer. If you want to run them on your own computer, see
+:doc:`User Installation <../how-to/installation/user-installation>` for
+instructions on how to install the requirements.
diff --git a/docs/tutorials/config.yaml b/docs/tutorials/config.yaml
new file mode 100644
index 0000000..00b68ad
--- /dev/null
+++ b/docs/tutorials/config.yaml
@@ -0,0 +1,15 @@
+type: pyaml.accelerator
+facility: pyAML test facility
+machine: pyaml test machine
+data_folder: null
+energy: 1000000000.0
+simulators:
+- type: pyaml.lattice.simulator
+ lattice: ${env:PYAML_TEST_LATTICE}
+ name: design
+devices:
+- type: pyaml.magnet.quadrupole
+ name: QF_001
+ model:
+ type: pyaml.magnet.identity_model
+ physics: ''
diff --git a/docs/tutorials/01_create_accelerator.py b/docs/tutorials/functionality/01_create_accelerator.py
similarity index 100%
rename from docs/tutorials/01_create_accelerator.py
rename to docs/tutorials/functionality/01_create_accelerator.py
diff --git a/docs/tutorials/02_inspect_accelerator.py b/docs/tutorials/functionality/02_inspect_accelerator.py
similarity index 96%
rename from docs/tutorials/02_inspect_accelerator.py
rename to docs/tutorials/functionality/02_inspect_accelerator.py
index 2fb9062..64f4e3e 100644
--- a/docs/tutorials/02_inspect_accelerator.py
+++ b/docs/tutorials/functionality/02_inspect_accelerator.py
@@ -41,4 +41,4 @@
# Show the Configuration of a Magnet
# ------------------------------------
-accelerator.design.magnets.get("QF_001")
+accelerator.design.magnet.get("QF_001")
diff --git a/docs/tutorials/functionality/README.rst b/docs/tutorials/functionality/README.rst
new file mode 100644
index 0000000..6b3c882
--- /dev/null
+++ b/docs/tutorials/functionality/README.rst
@@ -0,0 +1,6 @@
+Functionality
+-------------
+
+These tutorials cover the functionality of pyAML. They are designed to be
+followed in sequence, gradually increasing in complexity, but each tutorial is
+self-contained and can also be completed independently.
\ No newline at end of file
diff --git a/docs/tutorials/functionality/config.yaml b/docs/tutorials/functionality/config.yaml
new file mode 100644
index 0000000..00b68ad
--- /dev/null
+++ b/docs/tutorials/functionality/config.yaml
@@ -0,0 +1,15 @@
+type: pyaml.accelerator
+facility: pyAML test facility
+machine: pyaml test machine
+data_folder: null
+energy: 1000000000.0
+simulators:
+- type: pyaml.lattice.simulator
+ lattice: ${env:PYAML_TEST_LATTICE}
+ name: design
+devices:
+- type: pyaml.magnet.quadrupole
+ name: QF_001
+ model:
+ type: pyaml.magnet.identity_model
+ physics: ''
diff --git a/docs/tutorials/use_cases/README.rst b/docs/tutorials/use_cases/README.rst
new file mode 100644
index 0000000..e735d7e
--- /dev/null
+++ b/docs/tutorials/use_cases/README.rst
@@ -0,0 +1,4 @@
+Use Cases
+---------
+
+These tutorials demonstrate how pyAML can be used for specific use cases.
\ No newline at end of file
diff --git a/docs/tutorials/use_cases/chromaticity-measurement.py b/docs/tutorials/use_cases/chromaticity-measurement.py
new file mode 100644
index 0000000..23ee0da
--- /dev/null
+++ b/docs/tutorials/use_cases/chromaticity-measurement.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python
+# coding: utf-8
+# ---
+# jupyter:
+# jupytext:
+# cell_metadata_filter: -all
+# custom_cell_magics: kql
+# text_representation:
+# extension: .py
+# format_name: percent
+# format_version: '1.3'
+# jupytext_version: 1.11.2
+# kernelspec:
+# display_name: pyaml-documentation
+# language: python
+# name: python3
+# ---
+
+# %%
+"""Chromaticity Measurement
+============================
+
+This tutorial shows how to measure the chromaticity.
+"""
+
+# %%
+# Prerequisites
+# -------------
+#
+# The tutorial requires an existing pyAML configuration file which includes configuration
+# of the chromaticity monitor.
+#
+# The example uses the configuration provided by the ``pyaml-test-lattice`` package.
+
+from pyaml.common.constants import Action
+from pyaml_test_lattice import configurations
+
+# %%
+# Load the Accelerator
+# --------------------
+
+from pyaml.accelerator import Accelerator
+accelerator = Accelerator.load(configurations["pyaml/tango/pyaml-cs-oa/fodo_1gev_6d_pyaml-oa.yaml"])
+
+# Print the string representation of the accelerator
+print(accelerator)
+
+# %%
+# Control Mode Choice
+# --------------------
+#
+# - ``accelerator.design`` — runs pyAT locally, no control system needed. Set ``wait_time = 0.0``.
+# - ``accelerator.live`` — connects to the real machine or virtual twin. Set ``wait_time`` to allow readback settling (typically 1.5–2 s).
+#
+# For ``live`` control mode you need a running control system or virtual accelerator.
+# If you want to skip this, run the notebook in `design`` mode only.
+
+# Use the design mode
+SR = accelerator.design
+
+# For live mode
+# SR = accelerator.live
+
+wait_time = 0.0 if SR == accelerator.design else 2.0
+
+# Print the string representation of the simulator
+print(SR)
+
+# %%
+# Momentum Compaction Factor
+# --------------------------
+#
+# The momentum compaction factor is needed to convert RF frequency deviation to
+# momentum deviation. It is computed from the lattice model (design mode).
+
+accelerator.design.get_lattice().disable_6d()
+alphac = accelerator.design.get_lattice().get_mcf()
+accelerator.design.get_lattice().enable_6d()
+print(f"Momentum compaction factor: αc = {alphac:.6e}")
+
+# %%
+# Chromaticity Measurement
+# ------------------------
+#
+# The chromaticity monitor is named `CHROMATICITY_MONITOR` in the configuration.
+# The measurement sweeps the RF frequency and fits the resulting tune shift.
+#
+# Parameters:
+#
+# - ``alphac`` — momentum compaction factor (from lattice above)
+# - ``fit_order`` — polynomial fit order (2 = quadratic)
+# - ``n_step`` — number of RF frequency steps
+# - ``sleep_between_meas`` / ``sleep_between_step`` — settling times (set to ``wait_time`` for live mode)
+# - ``do_plot=True`` — show the tune vs. δp fit
+
+def chroma_callback(action: int, cb_data: dict):
+ if action == Action.MEASURE:
+ print(f"Chromaticity: #{cb_data['step']} RF={cb_data['rf']:.2f} Hz, Tune={cb_data['tune']}")
+ return True
+
+# %%
+chroma_monitor = SR.get_chromaticity_monitor("CHROMATICITY_MONITOR")
+
+# Measure the chromaticity
+chroma_monitor.measure(
+ callback=chroma_callback,
+ do_plot=True,
+ alphac=alphac,
+ fit_order=2,
+ n_step=5,
+ sleep_between_meas=wait_time,
+ sleep_between_step=wait_time,
+)
+
+# Get the measured chromaticity
+ksi = chroma_monitor.chromaticity.get()
+print(f"Measured chromaticity: ξx = {ksi[0]:.3f}, ξy = {ksi[1]:.3f}")
+
diff --git a/docs/tutorials/use_cases/orbit_correction.py b/docs/tutorials/use_cases/orbit_correction.py
new file mode 100644
index 0000000..f8e0898
--- /dev/null
+++ b/docs/tutorials/use_cases/orbit_correction.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python
+# coding: utf-8
+# ---
+# jupyter:
+# jupytext:
+# cell_metadata_filter: -all
+# custom_cell_magics: kql
+# text_representation:
+# extension: .py
+# format_name: percent
+# format_version: '1.3'
+# jupytext_version: 1.11.2
+# kernelspec:
+# display_name: pyaml-documentation
+# language: python
+# name: python3
+# ---
+
+# %%
+"""Orbit Correction
+=================
+
+This tutorial shows how to correct the orbit.
+"""
+
+# %%
+# Prerequisites
+# -------------
+#
+# The tutorial requires an existing pyAML configuration file which includes configuration
+# of the BPM, correctors and orbit correction.
+#
+# The example uses the configuration provided by the ``pyaml-test-lattice`` package.
+
+import numpy as np
+import matplotlib.pyplot as plt
+from time import sleep
+from pyaml.common.constants import Action
+from pyaml_test_lattice import configurations
+
+# %%
+# Load the Accelerator
+# --------------------
+
+from pyaml.accelerator import Accelerator
+accelerator = Accelerator.load(configurations["pyaml/tango/pyaml-cs-oa/fodo_1gev_6d_pyaml-oa.yaml"])
+
+# Print the string representation of the accelerator
+print(accelerator)
+
+
+# %%
+# Control Mode Choice
+# --------------------
+#
+# - ``accelerator.design`` — runs pyAT locally, no control system needed. Set ``wait_time = 0.0``.
+# - ``accelerator.live`` — connects to the real machine or virtual twin. Set ``wait_time`` to allow readback settling (typically 1.5–2 s).
+#
+# For ``live`` control mode you need a running control system or virtual accelerator.
+# If you want to skip this, run the notebook in `design`` mode only.
+
+# Use the design mode
+SR = accelerator.design
+
+# For live mode
+# SR = accelerator.live
+
+wait_time = 0.0 if SR == accelerator.design else 2.0
+
+# Print the string representation of the simulator
+print(SR)
+
+# %%
+# Orbit Diagnostics and Correctors
+# --------------------------------
+
+# Get BPMs
+bpms = SR.bpms.get("BPM")
+
+# Get correctors
+hcorr = SR.magnets.get("HCorr")
+vcorr = SR.magnets.get("VCorr")
+
+print(f"BPMs: {len(bpms)}, H correctors: {len(hcorr)}, V correctors: {len(vcorr)}")
+
+# %%
+# Orbit Response Matrix
+# ---------------------
+#
+# The ORM can be saved in multiple formats. Load it into ``SR.orbit`` before correcting.
+
+# Print string representation of the orbit response matrix
+print(SR.orm)
+
+# %%
+# Measure the ORM
+# ~~~~~~~~~~~~~~~~~~~~
+
+SR.orm.measure(sleep_between_step=wait_time)
+SR.orm.save("orm.json")
+SR.orm.save("orm.yaml", with_type="yaml")
+
+SR.orbit.load("orm.json")
+print("ORM measured, saved and loaded.")
+
+# %%
+# Visualise the ORM
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+orm_data = SR.orm.get()
+plt.imshow(np.array(orm_data["matrix"]))
+plt.colorbar()
+plt.title("Orbit response matrix")
+plt.xlabel("Corrector index")
+plt.ylabel("BPM index")
+plt.show()
+
+
+# %%
+# Dispersion Measurement
+# ----------------------
+#
+# The dispersion can optionally be measured and incorporated into the ORM to enable RF-based orbit
+# correction. This is particularly useful for correcting closed-orbit distortions caused
+# by energy errors.
+
+# %%
+def disp_callback(action: int, cb_data) -> bool:
+ from pyaml.common.constants import Action
+ if action == Action.APPLY:
+ print("Changing RF frequency")
+ elif action == Action.MEASURE:
+ print("Reading orbit")
+ elif action == Action.RESTORE:
+ print("Restoring RF frequency")
+ return True
+
+# Measure the dispersion
+SR.dispersion.measure(callback=disp_callback)
+disp_data = SR.dispersion.get()
+
+# Plot the results
+plt.plot(disp_data["frequency_response_x"], label="H dispersion")
+plt.plot(disp_data["frequency_response_y"], label="V dispersion")
+plt.xlabel("BPM index")
+plt.ylabel("Orbit / Δf [m/Hz]")
+plt.legend()
+plt.show()
+
+# %%
+# Introduce Orbit Distortion
+# ----------------------------
+#
+# Reset correctors to zero, then add small random kicks to simulate a disturbed orbit.
+
+# Reset correctors to zero
+hcorr.strengths.set(np.zeros(len(hcorr)))
+vcorr.strengths.set(np.zeros(len(vcorr)))
+ref_h, ref_v = bpms.positions.get().T
+reference = np.concatenate((ref_h, ref_v))
+
+# Add random kicks to simulate a distorted orbit
+np.random.seed(1)
+std_kick = 1e-6 # rad
+hcorr.strengths.set(hcorr.strengths.get() + std_kick * np.random.normal(size=len(hcorr)))
+vcorr.strengths.set(vcorr.strengths.get() + std_kick * np.random.normal(size=len(vcorr)))
+
+positions_bc = bpms.positions.get()
+std_bc = np.std(positions_bc, axis=0)
+print(f"R.m.s. orbit before correction — H: {1e6 * std_bc[0]:.1f} µm, V: {1e6 * std_bc[1]:.1f} µm")
+
+# %%
+# Orbit Before Correction
+# ------------------------
+
+plt.figure()
+plt.plot(positions_bc[:, 0] * 1e6, label="H (before)", color="C0", ls="--")
+plt.plot(positions_bc[:, 1] * 1e6, label="V (before)", color="C1", ls="--")
+plt.xlabel("BPM index")
+plt.ylabel("Position [µm]")
+plt.legend()
+plt.title("Orbit before correction")
+plt.show()
+
+
+# %%
+# Correct the Orbit
+# ------------------
+#
+# Standard correction: ``SR.orbit.correct(reference=reference)``.
+#
+# Optional variants (uncomment to use):
+#
+# - **Virtual corrector weight:** down-weights correctors that are already at large strengths.
+# - **RF correction:** also adjusts the RF frequency to minimise dispersion-driven orbit.
+
+# Standard correction
+SR.orbit.correct(reference=reference)
+
+# With virtual corrector weight (ESRF style):
+# SR.orbit.set_virtual_weight(1000)
+# SR.orbit.correct(reference=reference)
+
+# With RF orbit correction (ESRF style, requires dispersion in ORM):
+# SR.orbit.correct(reference=reference, rf=True)
+
+sleep(wait_time)
+
+positions_ac = bpms.positions.get()
+std_ac = np.std(positions_ac, axis=0)
+print(f"R.m.s. orbit after correction — H: {1e6 * std_ac[0]:.1f} µm, V: {1e6 * std_ac[1]:.1f} µm")
+
+fig, axes = plt.subplots(3, 1, figsize=(10, 8))
+
+axes[0].plot(positions_ac[:, 0] * 1e6, label="After", color="C0", ls="-")
+axes[0].plot(positions_bc[:, 0] * 1e6, label="Before", color="grey", ls="--")
+
+axes[0].set_ylabel("H position [µm]")
+axes[0].legend()
+
+axes[1].plot(positions_ac[:, 1] * 1e6, label="After", color="C1", ls="-")
+axes[1].plot(positions_bc[:, 1] * 1e6, label="Before", color="grey", ls="--")
+
+axes[1].set_ylabel("V position [µm]")
+axes[1].legend()
+
+axes[2].plot(hcorr.strengths.get() * 1e6, label="H correctors")
+axes[2].plot(vcorr.strengths.get() * 1e6, label="V correctors")
+axes[2].set_ylabel("Strength [µrad]")
+axes[2].set_xlabel("Index")
+axes[2].legend()
+
+fig.tight_layout()
+plt.show()
+
+# %%
+
diff --git a/docs/tutorials/use_cases/tune-correction.py b/docs/tutorials/use_cases/tune-correction.py
new file mode 100644
index 0000000..983b8e0
--- /dev/null
+++ b/docs/tutorials/use_cases/tune-correction.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+# ---
+# jupyter:
+# jupytext:
+# cell_metadata_filter: -all
+# custom_cell_magics: kql
+# text_representation:
+# extension: .py
+# format_name: percent
+# format_version: '1.3'
+# jupytext_version: 1.11.2
+# kernelspec:
+# display_name: pyaml-documentation
+# language: python
+# name: python3
+# ---
+
+# %%
+"""Tune Correction
+=================
+
+This tutorial shows how to read and correct the betatron tunes.
+"""
+
+# %%
+# Prerequisites
+# -------------
+#
+# The tutorial requires an existing pyAML configuration file which includes configuration
+# of the tune monitor and tune correction.
+#
+# The example uses the configuration provided by the ``pyaml-test-lattice`` package.
+
+# sphinx_gallery_thumbnail_path = '_static/tune_correction.svg'
+
+from pyaml.common.constants import Action
+from pyaml_test_lattice import configurations
+
+# %%
+# Load the Accelerator
+# --------------------
+
+from pyaml.accelerator import Accelerator
+accelerator = Accelerator.load(configurations["pyaml/tango/pyaml-cs-oa/fodo_1gev_6d_pyaml-oa.yaml"])
+
+# Print the string representation of the accelerator
+print(accelerator)
+
+# %%
+# Control Mode Choice
+# --------------------
+#
+# - ``accelerator.design`` — runs pyAT locally, no control system needed. Set ``wait_time = 0.0``.
+# - ``accelerator.live`` — connects to the real machine or virtual twin. Set ``wait_time`` to allow readback settling (typically 1.5–2 s).
+#
+# For ``live`` control mode you need a running control system or virtual accelerator.
+# If you want to skip this, run the notebook in `design`` mode only.
+
+# Use the design mode
+SR = accelerator.design
+
+# For live mode
+# SR = accelerator.live
+
+wait_time = 0.0 if SR == accelerator.design else 2.0
+
+# Print the string representation of the simulator
+print(SR)
+
+# %%
+# Betatron Tune Monitor
+# -----------------------
+#
+# The tune monitor is defined in the configuration file under the name `BETATRON_TUNE`.
+
+# %%
+tune_monitor = SR.get_betatron_tune_monitor("BETATRON_TUNE")
+
+# Print the string representation of the tune monitor
+print(tune_monitor)
+
+print(f"Current tune: {tune_monitor.tune.get()}")
+
+
+# %%
+# Quadrupolar Correctors
+# ----------------------
+#
+# The `QForTune` array contains the quadrupoles used for tune correction.
+# You can access and set strengths of individual correctors.
+
+
+# %%
+qcorrectors = SR.magnets.get("QForTune")
+first_q = qcorrectors[0]
+
+print(f"The ring has {len(qcorrectors)} quadrupolar correctors. First corrector: {first_q.get_name()}")
+
+# Print the string representation of the first quadrupole
+print(qcorrectors[0])
+
+# Get the current strength
+str_before = qcorrectors[0].strength.get()
+print(f"Current strength: {qcorrectors[0].strength.get()=:.4f}")
+
+# Set the strength
+qcorrectors[0].strength.set(str_before + 0.002)
+print(f"After stepping by 0.002: {qcorrectors[0].strength.get()=:.4f}")
+
+# Reset the strength
+qcorrectors[0].strength.set(str_before)
+print(f"Reset to {str_before:.4f}")
+
+# %%
+# Tune Correction
+# -----------------------------
+#
+# ``SR.tune`` is the `DEFAULT_TUNE_CORRECTION` tool. ``SR.trm`` is the `DEFAULT_TUNE_RESPONSE_MATRIX` tool.
+#
+# Before correcting the tune you need a response matrix. It can be measured as shown below or loaded from a previously saved file.
+
+# Print the string representation of the tune correction tool
+SR.tune
+
+# %%
+# Measure the Tune Response Matrix
+# ---------------------------------
+#
+# The callback below prints progress during the measurement. The ``sleep_between_step``` parameter controls the wait time between corrector steps — set it to 0 for design mode.
+#
+# **Note:** on some lattices ``disable_6d()`` is required before measuring in design mode since it only works in 4D.
+
+# Required on some lattices before measuring the TRM in design mode
+accelerator.design.get_lattice().disable_6d()
+
+def tune_callback(action: int, cb_data: dict):
+ if action == Action.MEASURE:
+ print(f"Tune response: #{cb_data['step']} {cb_data['magnet']} {cb_data['tune']}")
+ return True
+
+# Measure the response matrix
+SR.trm.measure(sleep_between_step=wait_time, callback=tune_callback)
+SR.trm.save("trm.json")
+
+# Load the measured response matrix
+SR.tune.load("trm.json")
+print("Response matrix loaded.")
+
+# Print string representation of the tune response matrix
+print(SR.trm)
+
+# %%
+# Correcting the tune
+# -----------------------------
+#
+# ``SR.tune.set([qx, qy])`` runs the correction iteratively.
+# The ``iter`` parameter controls the number of iterations and ``wait_time`` the settling time between each iteration.
+
+# %%
+print(f"Tune before correction: {SR.tune.readback()}")
+
+qx, qy = 0.19, 0.28
+print(f"\nSetting tune to [{qx}, {qy}]")
+SR.tune.set([qx, qy], iter=10, wait_time=wait_time)
+print(f"Tune after correction: {SR.tune.readback()}")
+
+qx, qy = 0.21, 0.30
+print(f"\nSetting tune to [{qx}, {qy}]")
+SR.tune.set([qx, qy], iter=10, wait_time=wait_time)
+print(f"Tune after correction: {SR.tune.readback()}")
+
+# Print the string representation of the tune correction
+SR.tune