diff --git a/.github/workflows/pySC_tests.yml b/.github/workflows/pySC_tests.yml new file mode 100644 index 00000000..c84a407d --- /dev/null +++ b/.github/workflows/pySC_tests.yml @@ -0,0 +1,29 @@ +name: Tests + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.12", "3.13", "3.14"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package with test dependencies + run: pip install ".[test]" + + - name: Run tests + run: pytest --cov diff --git a/.github/workflows/pyaml_tests.yml b/.github/workflows/pyaml_tests.yml new file mode 100644 index 00000000..fa825d66 --- /dev/null +++ b/.github/workflows/pyaml_tests.yml @@ -0,0 +1,40 @@ +name: pyAML tests + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.12", "3.14"] + + steps: + - uses: actions/checkout@v4 + + - name: Clone second repository + uses: actions/checkout@v4 + with: + repository: python-accelerator-middle-layer/pyaml + path: pyaml + ref: main + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package with test dependencies + run: | + pip install "./pyaml[test]" + pip install ./pyaml/tests/dummy_cs/tango-pyaml + pip install ".[test]" + + - name: Run tests + working-directory: pyaml + run: pytest -k "tuning" diff --git a/pySC/configuration/magnets_conf.py b/pySC/configuration/magnets_conf.py index ea65c2b5..8c1cb0cf 100644 --- a/pySC/configuration/magnets_conf.py +++ b/pySC/configuration/magnets_conf.py @@ -1,7 +1,8 @@ from typing import Any from ..core.simulated_commissioning import SimulatedCommissioning from ..core.lattice import ATLattice -from ..core.magnet import MAGNET_NAME_TYPE +from ..core.control import LinearConv +from ..core.magnet import ControlMagnetLink, MAGNET_NAME_TYPE from .general import get_error, get_indices_and_names from .supports_conf import generate_element_misalignments @@ -18,6 +19,14 @@ def generate_default_magnet_control(SC: SimulatedCommissioning, index: int, magn components_to_invert = dict.get(magnet_category_conf, 'invert', []).copy() # defaults to empty list if not declared # we need to copy because we remove elements later to check for undeclared components to invert + is_shifted = magnet_category_conf.get('shifted', False) + magnet_length = SC.lattice.get_length(index) + if is_shifted and not SC.lattice.is_dipole(index): + raise ValueError( + f"magnets/{magnet_category_name}: shifted=true expects a dipole " + f"at index {index} ({magnet_name})." + ) + if 'components' in magnet_category_conf: components = [] cal_errors = [] @@ -26,11 +35,13 @@ def generate_default_magnet_control(SC: SimulatedCommissioning, index: int, magn components.append(component) cal_errors.append(cal_error) - magnet_length = SC.lattice.get_length(index) magnet_settings.add_individually_powered_magnet( - sim_index=index, controlled_components=components, - magnet_name=magnet_name, magnet_length=magnet_length, - to_design=to_design) + sim_index=index, + controlled_components=components, + magnet_name=magnet_name, + magnet_length=magnet_length, + to_design=to_design + ) for component, cal_error in zip(components, cal_errors): control_name = f'{magnet_name}/{component}' @@ -59,7 +70,7 @@ def generate_default_magnet_control(SC: SimulatedCommissioning, index: int, magn offset = 0 setpoint = SC.lattice.get_magnet_component(index, component_type=component_type, order=order) if component[-1] == 'L': - length = SC.lattice.get_length(index) + length = magnet_length setpoint = setpoint * length if component in components_to_invert: @@ -70,6 +81,40 @@ def generate_default_magnet_control(SC: SimulatedCommissioning, index: int, magn magnet_settings.links[link_name].error.factor = factor magnet_settings.links[link_name].error.offset = offset + if is_shifted: + quad_components = [component for component in components if component in ('B2', 'B2L')] + if len(quad_components) != 1: + raise ValueError(f"magnets/{magnet_category_name}: shifted=true requires B2 or B2L in control") + + component = quad_components[0] + control_name = f'{magnet_name}/{component}' + source_link = magnet_settings.links[f'{control_name}->{control_name}'] + k1 = SC.lattice.get_magnet_component(index, component_type='B', order=1) + if k1 == 0: + raise ValueError(f"magnets/{magnet_category_name}: shifted=true requires a non-zero quadrupole component at index {index} ({magnet_name}).") + + k0 = SC.lattice.get_magnet_component(index, component_type='B', order=0) + reference_k0 = 0.0 + if type(SC.lattice) is ATLattice: + reference_k0 = SC.lattice.get_bending_angle(index) / magnet_length + k0 += reference_k0 + design_shift = k0 / k1 + + scale = magnet_length if source_link.is_integrated else 1.0 + factor = design_shift * source_link.error.factor / scale + offset = design_shift * source_link.error.offset / scale - reference_k0 + target_name = f'{magnet_name}/B1' + magnet_settings.add_link(ControlMagnetLink( + link_name=f'{control_name}->{target_name}', + magnet_name=magnet_name, + control_name=control_name, + component='B', + order=1, + error=LinearConv(factor=factor, offset=offset), + is_integrated=False, + )) + magnet_settings.magnets[magnet_name].offset_B[0] = 0.0 + assert len(components_to_invert) == 0, f"Found undeclared components in components to invert: magnets/{magnet_category_name}/invert: {components_to_invert}." @@ -113,4 +158,4 @@ def configure_magnets(SC: SimulatedCommissioning): SC.magnet_settings.connect_links() SC.magnet_settings.sendall() SC.design_magnet_settings.connect_links() - SC.design_magnet_settings.sendall() \ No newline at end of file + SC.design_magnet_settings.sendall() diff --git a/tests/configuration/test_magnets_conf.py b/tests/configuration/test_magnets_conf.py index 657b415b..ee2476c4 100644 --- a/tests/configuration/test_magnets_conf.py +++ b/tests/configuration/test_magnets_conf.py @@ -125,6 +125,54 @@ def test_configure_magnets_dipole_convention(hmba_lattice_file): assert link.error.offset == pytest.approx(expected_offset, rel=1e-6) +@pytest.mark.slow +@pytest.mark.parametrize("component", ["B2", "B2L"]) +def test_configure_shifted_quadrupole_feed_down(hmba_lattice_file, component): + lattice = ATLattice(lattice_file=hmba_lattice_file, naming="FamName") + dip_name = "DQ1B" + dip_index = 50 + initial_angle = lattice.get_bending_angle(dip_index) + initial_b0 = lattice.get_magnet_component(dip_index, "B", 0) + initial_k1 = lattice.get_magnet_component(dip_index, "B", 1) + length = lattice.get_length(dip_index) + design_shift = (initial_angle / length + initial_b0) / initial_k1 + + config = { + "error_table": {"quad_cal": "0"}, + "magnets": { + "shifted_quadrupole": { + "regex": f"^{dip_name}$", + "components": [{component: "quad_cal"}], + "shifted": True, + }, + }, + } + SC = SimulatedCommissioning(lattice=lattice, configuration=config, seed=42) + configure_magnets(SC) + + control_name = f"{dip_name}/{component}" + feed_down_link = SC.magnet_settings.links[f"{control_name}->{dip_name}/B1"] + assert feed_down_link.is_integrated is False + assert SC.lattice.get_bending_angle(dip_index) == pytest.approx(initial_angle) + assert SC.lattice.get_magnet_component( + dip_index, "B", 0, use_design=False + ) == pytest.approx(initial_b0) + + delta_k1 = 0.2 + setpoint = initial_k1 + delta_k1 + if component == "B2L": + setpoint *= length + SC.magnet_settings.set(control_name, setpoint) + + assert SC.lattice.get_bending_angle(dip_index) == pytest.approx(initial_angle) + assert SC.lattice.get_magnet_component( + dip_index, "B", 1, use_design=False + ) == pytest.approx(initial_k1 + delta_k1) + assert SC.lattice.get_magnet_component( + dip_index, "B", 0, use_design=False + ) == pytest.approx(initial_b0 + design_shift * delta_k1) + + @pytest.mark.slow def test_configure_magnets_limits(hmba_lattice_file): """Controls have limits when configured.""" diff --git a/tests/core/test_magnet.py b/tests/core/test_magnet.py index 4ae2dba5..69bf5ea4 100644 --- a/tests/core/test_magnet.py +++ b/tests/core/test_magnet.py @@ -1,7 +1,6 @@ """Tests for pySC.core.magnet: Magnet, ControlMagnetLink.""" import pytest -from unittest.mock import MagicMock, PropertyMock - +from unittest.mock import MagicMock from pySC.core.magnet import Magnet, ControlMagnetLink from pySC.core.control import Control, LinearConv