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 @@ + + Tune correction response matrix + A stylized accelerator ring with quadrupole correctors feeding a tune response matrix and converging tune plot. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TUNE CORRECTION + + + + + + + + + + + + + + + + + + + + + + + TUNE RESPONSE + QForTune → BETATRON_TUNE + + + + + + + + + corrector strength + tune shift + + + ∂Qx / ∂K∂Qy / ∂Kpseudoinverse + + + + + + + CONVERGENCE + measured tune → target tune + + + + + + target + iteration + + + + + + + + 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