From 9890205be3a85b301b430c1aabb125ea91c16a71 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Mon, 28 Jul 2025 18:51:00 +0200 Subject: [PATCH 1/5] Basic dummy control system first implementation --- tests/conftest.py | 26 +++++--- tests/control/__init__.py | 0 .../dummy-cs-pyaml/dummy-cs-pyaml/__init__.py | 11 ++++ .../dummy-cs-pyaml/dummy_controlsystem.py | 36 ++++++++++ .../{ => dummy-cs-pyaml}/dummy_device.py | 7 +- tests/test_aml.py | 37 ++++++----- tests/test_cs_interactions.py | 10 +++ tests/test_tune.py | 65 ++++++++++--------- 8 files changed, 129 insertions(+), 63 deletions(-) create mode 100644 tests/control/__init__.py create mode 100644 tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py create mode 100644 tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py rename tests/control/dummy-cs-pyaml/{ => dummy-cs-pyaml}/dummy_device.py (90%) create mode 100644 tests/test_cs_interactions.py diff --git a/tests/conftest.py b/tests/conftest.py index 2fd7eca4..aec077e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,31 +13,37 @@ def install_test_package(request): The test must provide a dictionary as parameter with: - 'name': name of the installable package (used for pip uninstall) - - 'path': relative path to the package folder (e.g. 'tests/mon_dir') + - 'path': relative path to the package folder (e.g. 'tests/my_dir'). Optional, replaced by package name if absent. Example: -------- @pytest.mark.parametrize("install_test_package", [{ - "name": "mon_package", - "path": "tests/mon_dir" + "name": "my_package", + "path": "tests/my_dir" }], indirect=True) def test_x(install_test_package): ... """ info = request.param package_name = info["name"] - package_path = pathlib.Path(info["path"]).resolve() - - if not package_path.exists(): - raise FileNotFoundError(f"Package path not found: {package_path}") + package_path = None + if info["path"] is not None: + package_path = pathlib.Path(info["path"]).resolve() + if not package_path.exists(): + raise FileNotFoundError(f"Package path not found: {package_path}") if not ((package_path / "pyproject.toml").exists() or (package_path / "setup.py").exists()): raise RuntimeError(f"No pyproject.toml or setup.py found in {package_path}") # Install package - subprocess.check_call([ - sys.executable, "-m", "pip", "install", "--quiet", "--editable", str(package_path) - ]) + if package_path is not None: + subprocess.check_call([ + sys.executable, "-m", "pip", "install", "--quiet", "--editable", str(package_path) + ]) + else: + subprocess.check_call([ + sys.executable, "-m", "pip", "install", "--quiet", "--editable", str(package_name) + ]) # Test the import. import importlib # Ensure its path is importable diff --git a/tests/control/__init__.py b/tests/control/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py new file mode 100644 index 00000000..d801e766 --- /dev/null +++ b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py @@ -0,0 +1,11 @@ +""" +PyAML External module example +""" + +__title__ = "pyAML Dummy Control System" +__description__ = "PyAML Dummy Control System" +__url__ = "https://github.com/python-accelerator-middle-layer/pyaml" +__version__ = "0.0.0" +__author__ = "pyAML collaboration" +__author_email__ = "" +__all__ = [__version__] diff --git a/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py new file mode 100644 index 00000000..df8854a3 --- /dev/null +++ b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from pyaml.control.controlsystem import ControlSystem + + +class ConfigModel(BaseModel): + """ + Configuration model for a Tango Control System. + + Attributes + ---------- + name : str + Name of the control system. + """ + name: str + +class DummyControlSystem(ControlSystem): + + def __init__(self, cfg: ConfigModel): + super().__init__() + self._cfg = cfg + + + def init_cs(self): + pass + + + def name(self) -> str: + """ + Return the name of the control system. + + Returns + ------- + str + Name of the control system. + """ + return self._cfg.name diff --git a/tests/control/dummy-cs-pyaml/dummy_device.py b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_device.py similarity index 90% rename from tests/control/dummy-cs-pyaml/dummy_device.py rename to tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_device.py index 1449c429..c93d5d77 100644 --- a/tests/control/dummy-cs-pyaml/dummy_device.py +++ b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_device.py @@ -23,6 +23,7 @@ class DummyDevice(DeviceAccess): """ def __init__(self, cfg: ConfigModel): + super().__init__() self._cfg = cfg self._setpoint = cfg.setpoint self._readback = cfg.readback @@ -36,12 +37,12 @@ def measure_name(self) -> str: return self._readback def set(self, value: float): - self._cache = value + self._cache = Value(value) def set_and_wait(self, value: float): - self._cache = value + self.set(value) - def get(self) -> float: + def get(self) -> Value: return self._cache def readback(self) -> Value: diff --git a/tests/test_aml.py b/tests/test_aml.py index 7564ad4b..c38996e7 100644 --- a/tests/test_aml.py +++ b/tests/test_aml.py @@ -6,27 +6,28 @@ from pyaml.magnet.model import MagnetModel import at -#def test_aml(config_root_dir): -set_root_folder("tests/config") +if False: + #def test_aml(config_root_dir): + set_root_folder("tests/config") -ml:PyAML = pyaml("sr.yaml") -sr:Instrument = ml.get('sr') -sr.design.get_lattice().disable_6d() -sr.design.get_magnet(MagnetType.HCORRECTOR,"SH1A-C01-H").strength.set(0.000010) -o,_ = sr.design.get_lattice().find_orbit() -print(o) + ml:PyAML = pyaml("sr.yaml") + sr:Instrument = ml.get('sr') + sr.design.get_lattice().disable_6d() + sr.design.get_magnet(MagnetType.HCORRECTOR,"SH1A-C01-H").strength.set(0.000010) + o,_ = sr.design.get_lattice().find_orbit() + print(o) -pcurrent = sr.design.get_magnet(MagnetType.HCORRECTOR,"SH1A-C01-H").hardware.get() -print(pcurrent) -model:MagnetModel = sr.design.get_magnet(MagnetType.COMBINED,"SH1A-C01").model -rcurrents = model.compute_hardware_values([0.000010,0,0]) -print(rcurrents) -print(sr.design.get_magnet(MagnetType.HCORRECTOR,"SH1A-C01-H").strength.unit()) -print(sr.design.get_magnet(MagnetType.HCORRECTOR,"SH1A-C01-H").hardware.unit()) + pcurrent = sr.design.get_magnet(MagnetType.HCORRECTOR,"SH1A-C01-H").hardware.get() + print(pcurrent) + model:MagnetModel = sr.design.get_magnet(MagnetType.COMBINED,"SH1A-C01").model + rcurrents = model.compute_hardware_values([0.000010,0,0]) + print(rcurrents) + print(sr.design.get_magnet(MagnetType.HCORRECTOR,"SH1A-C01-H").strength.unit()) + print(sr.design.get_magnet(MagnetType.HCORRECTOR,"SH1A-C01-H").hardware.unit()) -sr.design.get_magnets("HCORR").strengths.set([0.000010,-0.000010]) -o,_ = sr.design.get_lattice().find_orbit() -print(o) + sr.design.get_magnets("HCORR").strengths.set([0.000010,-0.000010]) + o,_ = sr.design.get_lattice().find_orbit() + print(o) diff --git a/tests/test_cs_interactions.py b/tests/test_cs_interactions.py new file mode 100644 index 00000000..e8a91c05 --- /dev/null +++ b/tests/test_cs_interactions.py @@ -0,0 +1,10 @@ +import pytest + + +@pytest.mark.parametrize("install_test_package", [{ + "name": "dummy-cs-pyaml", + "path": "tests/control/dummy-cs-pyaml" +}], indirect=True) +def test_dummy_cs_pyaml(install_test_package): + print("Hello there !") + assert True diff --git a/tests/test_tune.py b/tests/test_tune.py index 0e43f99b..53588e86 100644 --- a/tests/test_tune.py +++ b/tests/test_tune.py @@ -7,35 +7,36 @@ import numpy as np import at -#def test_aml(config_root_dir): -set_root_folder("tests/config") - -ml:PyAML = pyaml("EBSTune.yaml") -sr:Instrument = ml.get('sr') -sr.design.get_lattice().disable_6d() - -quadForTune = sr.design.get_magnets("QForTune") - -# Build tune response matrix -tune = sr.design.get_lattice().get_tune() -print(tune) -tunemat = np.zeros((len(quadForTune),2)) - -for idx,m in enumerate(quadForTune): - str = m.strength.get() - m.strength.set(str+1e-4) - dq = sr.design.get_lattice().get_tune() - tune - tunemat[idx] = dq*1e4 - m.strength.set(str) - -# Compute correction matrix -correctionmat = np.linalg.pinv(tunemat.T) - -# Correct tune -strs = quadForTune.strengths.get() -strs += np.matmul(correctionmat,[0.1,0.05]) # Ask for correction [dqx,dqy] -quadForTune.strengths.set(strs) -newTune = sr.design.get_lattice().get_tune() -print(newTune-tune) # Expext someting close to [0.1,0.05] - -#pyaml.configuration.factory._ALL_ELEMENTS.clear() +if False: + #def test_aml(config_root_dir): + set_root_folder("tests/config") + + ml:PyAML = pyaml("EBSTune.yaml") + sr:Instrument = ml.get('sr') + sr.design.get_lattice().disable_6d() + + quadForTune = sr.design.get_magnets("QForTune") + + # Build tune response matrix + tune = sr.design.get_lattice().get_tune() + print(tune) + tunemat = np.zeros((len(quadForTune),2)) + + for idx,m in enumerate(quadForTune): + str = m.strength.get() + m.strength.set(str+1e-4) + dq = sr.design.get_lattice().get_tune() - tune + tunemat[idx] = dq*1e4 + m.strength.set(str) + + # Compute correction matrix + correctionmat = np.linalg.pinv(tunemat.T) + + # Correct tune + strs = quadForTune.strengths.get() + strs += np.matmul(correctionmat,[0.1,0.05]) # Ask for correction [dqx,dqy] + quadForTune.strengths.set(strs) + newTune = sr.design.get_lattice().get_tune() + print(newTune-tune) # Expext someting close to [0.1,0.05] + + #pyaml.configuration.factory._ALL_ELEMENTS.clear() From 1ef8f9591c0128d37769ea3e19b01274e69a69b9 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Wed, 30 Jul 2025 14:49:36 +0200 Subject: [PATCH 2/5] New exception management in config load allow to find error origin in the config tree. --- pyaml/configuration/config_exception.py | 48 +++++++++++++++++++ pyaml/configuration/factory.py | 63 ++++++++++++++++--------- pyaml/exception.py | 2 +- 3 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 pyaml/configuration/config_exception.py diff --git a/pyaml/configuration/config_exception.py b/pyaml/configuration/config_exception.py new file mode 100644 index 00000000..87e19f7b --- /dev/null +++ b/pyaml/configuration/config_exception.py @@ -0,0 +1,48 @@ +from typing import Union +from pyaml.exception import PyAMLException + + +class PyAMLConfigException(PyAMLException): + """Exception raised for custom error scenarios. + + Attributes: + message -- explanation of the error + """ + + def __init__(self, config_key = None, parent_exception:Union["PyAMLConfigException", "PyAMLException"] = None): + self.parent_keys = [] + self.config_key = config_key + self.parent_exception = parent_exception + message = "An exception occurred while building object." + if parent_exception is not None: + if isinstance(self.parent_exception, PyAMLConfigException) and parent_exception.config_key is not None: + self.parent_keys.append(parent_exception.config_key) + self.parent_keys.extend(parent_exception.parent_keys) + if config_key is not None: + message = f"An exception occurred while building key '{config_key}.{parent_exception.get_keys()}': {parent_exception.get_original_message()}" + else: + message = f"An exception occurred while building object in '{parent_exception.get_keys()}': {parent_exception.get_original_message()}" + else: + if config_key is not None: + message = f"An exception occurred while building key '{config_key}': {parent_exception.message}" + else: + message = f"An exception occurred while building object: {parent_exception.message}" + super().__init__(message) + + def get_keys(self) -> str: + keys = "" + if self.config_key is not None: + if len(self.parent_keys)>0: + keys = ".".join(self.parent_keys) + keys += "." + keys += self.config_key + return keys + + def get_original_message(self): + if self.parent_exception is not None: + if isinstance(self.parent_exception, PyAMLConfigException): + return self.parent_exception.get_original_message() + else: + return self.parent_exception.message + else: + return self.message \ No newline at end of file diff --git a/pyaml/configuration/factory.py b/pyaml/configuration/factory.py index 9b11fd16..049903cc 100644 --- a/pyaml/configuration/factory.py +++ b/pyaml/configuration/factory.py @@ -2,6 +2,9 @@ import importlib import pprint as pp import traceback + +from .config_exception import PyAMLConfigException +from ..exception import PyAMLException from ..lattice.element import Element #TODO: @@ -13,23 +16,26 @@ def buildObject(d:dict): """Build an object from the dict""" if not isinstance(d,dict): - raise Exception("Unecpted object " + str(d)) + raise PyAMLException("Unexpected object " + str(d)) if not "type" in d: - raise Exception("No type specified for " + str(type(d)) + ":" + str(d)) - typeStr = d["type"] + raise PyAMLException("No type specified for " + str(type(d)) + ":" + str(d)) + type_str = d["type"] del d["type"] - module = importlib.import_module(typeStr) + try: + module = importlib.import_module(type_str) + except ModuleNotFoundError as ex: + raise PyAMLException(f"Module referenced in type cannot be founded: '{type_str}'") from ex # Get the config object config_cls = getattr(module, "ConfigModel", None) if config_cls is None: - raise ValueError(f"ConfigModel class '{typeStr}.ConfigModel' not found") + raise ValueError(f"ConfigModel class '{type_str}.ConfigModel' not found") # Get the class name cls_name = getattr(module, "PYAMLCLASS", None) if cls_name is None: - raise ValueError(f"PYAMLCLASS definition not found in '{typeStr}'") + raise ValueError(f"PYAMLCLASS definition not found in '{type_str}'") try: @@ -40,7 +46,7 @@ def buildObject(d:dict): elem_cls = getattr(module, cls_name, None) if elem_cls is None: raise ValueError( - f"Unknown element class '{typeStr}.{cls_name}'" + f"Unknown element class '{type_str}.{cls_name}'" ) obj = elem_cls(cfg) @@ -51,7 +57,7 @@ def buildObject(d:dict): print(traceback.format_exc()) print(e) - print(typeStr) + print(type_str) pp.pprint(d) #Fatal quit() @@ -61,38 +67,53 @@ def depthFirstBuild(d): """Main factory function (Depth-first factory)""" if isinstance(d,list): - # list can be a list of objects or a list of native types l = [] - for e in d: + for index, e in enumerate(d): if isinstance(e,dict) or isinstance(e,list): - obj = depthFirstBuild(e) - l.append(obj) + try: + obj = depthFirstBuild(e) + l.append(obj) + except PyAMLException as pyaml_ex: + raise PyAMLConfigException(f"[{index}]", pyaml_ex) from pyaml_ex + except Exception as ex: + raise PyAMLConfigException(f"[{index}]") from ex else: l.append(e) return l elif isinstance(d,dict): - for key, value in d.items(): if isinstance(value,dict) or isinstance(value,list): - obj = depthFirstBuild(value) - # Replace the inner dict by the object itself - d[key]=obj - - # We are now on leaf (no nested object), we can construt - obj = buildObject(d) + try: + obj = depthFirstBuild(value) + # Replace the inner dict by the object itself + d[key]=obj + except PyAMLException as pyaml_ex: + raise PyAMLConfigException(key, pyaml_ex) from pyaml_ex + except Exception as ex: + raise PyAMLConfigException(key) from ex + + # We are now on leaf (no nested object), we can construct + try: + obj = buildObject(d) + except PyAMLException as pyaml_ex: + raise PyAMLConfigException(None, pyaml_ex) from pyaml_ex + except Exception as ex: + raise PyAMLException("An exception occurred while building object") from ex return obj + raise PyAMLException("Unexpected element found.") + def register_element(elt): if isinstance(elt,Element): name = str(elt) if name in _ALL_ELEMENTS: - raise Exception(f"element {name} already defined") + raise PyAMLException(f"element {name} already defined") _ALL_ELEMENTS[name] = elt def get_element(name:str): if name not in _ALL_ELEMENTS: - raise Exception(f"element {name} not defined") + raise PyAMLException(f"element {name} not defined") return _ALL_ELEMENTS[name] diff --git a/pyaml/exception.py b/pyaml/exception.py index 8c57c751..35ceaeff 100644 --- a/pyaml/exception.py +++ b/pyaml/exception.py @@ -6,5 +6,5 @@ class PyAMLException(Exception): """ def __init__(self, message): + super().__init__(message) self.message = message - super().__init__(self.message) From 29ee7fbc936e008a3bf461e21229af683514aac7 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Wed, 30 Jul 2025 14:50:06 +0200 Subject: [PATCH 3/5] Corrections in the dummy control system. --- tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py | 2 +- .../dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py index d801e766..d271102a 100644 --- a/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py +++ b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py @@ -1,5 +1,5 @@ """ -PyAML External module example +PyAML Dummy control system for tests. """ __title__ = "pyAML Dummy Control System" diff --git a/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py index df8854a3..812a22de 100644 --- a/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py +++ b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py @@ -1,6 +1,7 @@ from pydantic import BaseModel from pyaml.control.controlsystem import ControlSystem +PYAMLCLASS = "DummyControlSystem" class ConfigModel(BaseModel): """ From 274f0c91b00c7de9c7c81fe7b5cbc905ca3efaa7 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Wed, 30 Jul 2025 14:50:30 +0200 Subject: [PATCH 4/5] Tests refactoring. --- tests/config/sr/quadrupoles/QEXT.yaml | 2 +- tests/test_tune.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/config/sr/quadrupoles/QEXT.yaml b/tests/config/sr/quadrupoles/QEXT.yaml index 9f51c1a3..435ce078 100644 --- a/tests/config/sr/quadrupoles/QEXT.yaml +++ b/tests/config/sr/quadrupoles/QEXT.yaml @@ -1,7 +1,7 @@ type: pyaml.magnet.magnet name: IDCORR model: - type: pyaml_external.external_model + type: pyaml_external.external_unitconv param: my random param powersupply: type: pyaml.control.device diff --git a/tests/test_tune.py b/tests/test_tune.py index 53588e86..30cbd18f 100644 --- a/tests/test_tune.py +++ b/tests/test_tune.py @@ -1,3 +1,4 @@ +import pyaml from pyaml.pyaml import pyaml,PyAML from pyaml.instrument import Instrument from pyaml.configuration import set_root_folder @@ -7,7 +8,7 @@ import numpy as np import at -if False: +def test_ebs_tune(): #def test_aml(config_root_dir): set_root_folder("tests/config") @@ -23,11 +24,11 @@ tunemat = np.zeros((len(quadForTune),2)) for idx,m in enumerate(quadForTune): - str = m.strength.get() - m.strength.set(str+1e-4) + strength = m.strength.get() + m.strength.set(strength+1e-4) dq = sr.design.get_lattice().get_tune() - tune tunemat[idx] = dq*1e4 - m.strength.set(str) + m.strength.set(strength) # Compute correction matrix correctionmat = np.linalg.pinv(tunemat.T) @@ -39,4 +40,4 @@ newTune = sr.design.get_lattice().get_tune() print(newTune-tune) # Expext someting close to [0.1,0.05] - #pyaml.configuration.factory._ALL_ELEMENTS.clear() + pyaml.configuration.factory._ALL_ELEMENTS.clear() From abdc25a75073618bffb7e34efede7a7e31970e0b Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Wed, 30 Jul 2025 15:03:50 +0200 Subject: [PATCH 5/5] Correction of merge errors. --- tests/dummy_cs/tango/tango/pyaml/attribute.py | 11 +---------- tests/test_cs_interactions.py | 10 ---------- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 tests/test_cs_interactions.py diff --git a/tests/dummy_cs/tango/tango/pyaml/attribute.py b/tests/dummy_cs/tango/tango/pyaml/attribute.py index 5dc843db..a2da8ec0 100644 --- a/tests/dummy_cs/tango/tango/pyaml/attribute.py +++ b/tests/dummy_cs/tango/tango/pyaml/attribute.py @@ -9,14 +9,10 @@ class ConfigModel(BaseModel): unit: str = "" class Attribute(DeviceAccess): - def __init__(self, cfg: ConfigModel): - super().__init__(cfg) - """ Class that implements a default device class that just prints out values (Debugging purpose) """ - def __init__(self, cfg: ConfigModel): super().__init__() self._cfg = cfg @@ -32,17 +28,12 @@ def measure_name(self) -> str: return self._readback def set(self, value: float): -<<<<<<<< HEAD:tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_device.py - self._cache = Value(value) -======== - print(f"{self._setpoint}: set {value}") self._cache = value ->>>>>>>> origin/main:tests/dummy_cs/tango/tango/pyaml/attribute.py def set_and_wait(self, value: float): self.set(value) - def get(self) -> Value: + def get(self) -> float: return self._cache def readback(self) -> Value: diff --git a/tests/test_cs_interactions.py b/tests/test_cs_interactions.py deleted file mode 100644 index e8a91c05..00000000 --- a/tests/test_cs_interactions.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - - -@pytest.mark.parametrize("install_test_package", [{ - "name": "dummy-cs-pyaml", - "path": "tests/control/dummy-cs-pyaml" -}], indirect=True) -def test_dummy_cs_pyaml(install_test_package): - print("Hello there !") - assert True