mosca-client 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ pixi.lock
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Ignore any hidden files from the project root
12
+ /.*
13
+ !.gitlab-ci.yml
14
+
15
+ conda-bld/
16
+
17
+ # Distribution / packaging
18
+ .Python
19
+ env/
20
+ build/
21
+ develop-eggs/
22
+ dist/
23
+ downloads/
24
+ eggs/
25
+ .eggs/
26
+ lib/
27
+ lib64/
28
+ parts/
29
+ sdist/
30
+ var/
31
+ wheels/
32
+ *.egg-info/
33
+ .installed.cfg
34
+ *.egg
35
+
36
+ # PyInstaller
37
+ # Usually these files are written by a python script from a template
38
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
39
+ *.manifest
40
+ *.spec
41
+
42
+ # Installer logs
43
+ pip-log.txt
44
+ pip-delete-this-directory.txt
45
+
46
+ # Unit test / coverage reports
47
+ htmlcov*/
48
+ .tox/
49
+ .coverage
50
+ .coverage.*
51
+ .cache
52
+ nosetests.xml
53
+ coverage.xml
54
+ report.xml
55
+ *.cover
56
+ .hypothesis/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # pyenv
83
+ .python-version
84
+
85
+ # celery beat schedule file
86
+ celerybeat-schedule
87
+
88
+ # SageMath parsed files
89
+ *.sage.py
90
+
91
+ # dotenv
92
+ .env
93
+
94
+ # virtualenv
95
+ .venv
96
+ venv/
97
+ ENV/
98
+
99
+ # Spyder project settings
100
+ .spyderproject
101
+ .spyproject
102
+
103
+ # Rope project settings
104
+ .ropeproject
105
+
106
+ # mkdocs documentation
107
+ doc/site/
108
+
109
+ # mypy
110
+ .mypy_cache/
111
+
112
+ *.mprg
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.5
2
+ Name: mosca-client
3
+ Version: 2.0.2
4
+ Requires-Python: <3.13,>=3.10
5
+ Requires-Dist: numpy
6
+ Requires-Dist: pytango
7
+ Requires-Dist: ruamel-yaml
8
+ Requires-Dist: tabulate
@@ -0,0 +1,4 @@
1
+ MOSCA Client
2
+ ============
3
+
4
+ Client modules for MOSCA MCA detectors.
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "mosca-client"
3
+ requires-python = ">=3.10,<3.13"
4
+ version = "2.0.2"
5
+
6
+ dependencies = [
7
+ "pytango",
8
+ "numpy",
9
+ "tabulate",
10
+ "ruamel.yaml"
11
+ ]
12
+
13
+ [build-system]
14
+ build-backend = "hatchling.build"
15
+ requires = ["hatchling"]
16
+
17
+ [tool.pixi.package]
18
+ name = "mosca-client"
19
+ version = { workspace = true }
20
+
21
+ [tool.pixi.package.build]
22
+ backend = { name = "pixi-build-python", version = "0.*" }
23
+
24
+ [tool.pixi.package.host-dependencies]
25
+ hatchling = "==1.26.3"
26
+
27
+ [tool.pixi.pypi-dependencies]
28
+ mosca_client = { path = ".", editable = true }
File without changes
File without changes
@@ -0,0 +1,116 @@
1
+ import time
2
+
3
+
4
+ from ..mca import McaBase
5
+
6
+
7
+ # from .base import McaController, TriggerMode
8
+
9
+
10
+ class Dante(McaBase):
11
+ """XGLab's Dante mosca 2 client."""
12
+
13
+ # Bounds for config_file's wait loop, in seconds.
14
+ CONFIG_LOAD_START_WAIT = 5.
15
+ CONFIG_LOAD_TIMEOUT = 60.
16
+
17
+ def __init__(self, *args, **kwargs):
18
+ super().__init__(*args, **kwargs)
19
+
20
+ def __info__(self) -> str:
21
+ """
22
+ Add specific information for XIA controllers.
23
+ """
24
+ txt = super().__info__()
25
+ txt += f"\n * config file: {self.config_file}\n"
26
+ txt += f" * gating mode: {self.gating_mode}\n"
27
+ return txt
28
+
29
+ # def _apply_settings(self, config_file=None, **kwargs):
30
+ # if config_file is not None:
31
+ # self.config_file = config_file
32
+ # return super()._apply_settings(**kwargs)
33
+
34
+ def stats_mapping(self):
35
+ return {
36
+ "realtime": "elapsed_time",
37
+ "trigger_livetime": "trigger_livetime",
38
+ "icr": "icr",
39
+ "ocr": "ocr",
40
+ }
41
+
42
+ @property
43
+ def gating_mode(self):
44
+ return self.proxy.gating_mode
45
+
46
+ def available_configurations(self):
47
+ return self.proxy.available_configurations
48
+
49
+ def _load_configuration(self, configuration: str | None = None) -> None:
50
+ proxy = self.proxy
51
+ print(f"Setting config file to {configuration}. This may take a while.")
52
+ proxy.set_configuration_file_async(configuration)
53
+
54
+ # Wait for the server to actually start processing the request
55
+ # (mca_status flips to UNINITIALIZED) instead of blindly sleeping
56
+ # for a fixed amount of time -- avoids racing against slow thread
57
+ # scheduling on the server side, which could otherwise let a stale
58
+ # prior status (e.g. READY) slip past the checks below undetected.
59
+ start_deadline = time.time() + self.CONFIG_LOAD_START_WAIT
60
+ while proxy.mca_status != "UNINITIALIZED" and time.time() < start_deadline:
61
+ time.sleep(0.1)
62
+
63
+ if proxy.mca_status != "UNINITIALIZED":
64
+ raise RuntimeError(
65
+ f"Server did not start processing config [{configuration}] "
66
+ f"(mca_status still {proxy.mca_status!r} after "
67
+ f"{self.CONFIG_LOAD_START_WAIT}s)."
68
+ )
69
+
70
+ deadline = time.time() + self.CONFIG_LOAD_TIMEOUT
71
+ spin = ["\\", "-", "/", "-"]
72
+ i = 0
73
+ while proxy.mca_status == "UNINITIALIZED":
74
+ if time.time() > deadline:
75
+ raise RuntimeError(
76
+ f"Timed out waiting for config [{configuration}] to load "
77
+ f"(still UNINITIALIZED after {self.CONFIG_LOAD_TIMEOUT}s)."
78
+ )
79
+ init_msg = proxy.init_message
80
+ print(f"\033[KIn progress [{spin[i%4]}] {init_msg} ", end="\r")
81
+ time.sleep(0.3)
82
+ i = (i + 1) % 4
83
+
84
+ init_msg = proxy.init_message
85
+ print(f"\033[K{init_msg}")
86
+ if proxy.mca_status != "READY":
87
+ raise RuntimeError(
88
+ f"Failed to load config [{configuration}]. Please check server logs."
89
+ )
90
+ return self.current_configuration()
91
+
92
+ def current_configuration(self):
93
+ return self.proxy.configuration_file
94
+
95
+ @property
96
+ def config_files_list(self):
97
+ # legacy code
98
+ return self.available_configurations()
99
+
100
+ @property
101
+ def config_file(self):
102
+ # legacy code
103
+ return self.current_configuration()
104
+
105
+ @config_file.setter
106
+ def config_file(self, config_file):
107
+ # legacy code
108
+ self.load_configuration(config_file)
109
+
110
+ @property
111
+ def config_dir(self):
112
+ # legacy code
113
+ return self.proxy.configuration_directory
114
+
115
+
116
+ Mca = Dante
@@ -0,0 +1,8 @@
1
+ from .dante import Dante
2
+
3
+
4
+ class DantePlus(Dante):
5
+ pass
6
+
7
+
8
+ Mca = DantePlus
@@ -0,0 +1,9 @@
1
+ from ..mca import McaBase
2
+ from .simulation import SimulatorMixin
3
+
4
+
5
+ class Dummy(SimulatorMixin, McaBase):
6
+ pass
7
+
8
+
9
+ Mca = Dummy
@@ -0,0 +1,71 @@
1
+ import numpy as np
2
+
3
+ from .xia import Xia
4
+
5
+
6
+ class FalconX(Xia):
7
+ def __info__(self) -> str:
8
+ txt = super().__info__()
9
+ txt += f" * mca_refresh: {self.proxy.mca_refresh:.4f} s\n"
10
+ return txt
11
+
12
+ def scan_metadata(self) -> dict:
13
+ metadata = super().scan_metadata()
14
+ metadata["mca_refresh"] = self.proxy.mca_refresh
15
+ return metadata
16
+
17
+ def filter_parameters(self,
18
+ acq_realtime=None,
19
+ block_size=None,
20
+ trigger_mode=None,
21
+ mca_refresh=None,
22
+ **kwargs):
23
+ """ Computes mca_refresh and block_size if not provided.
24
+ * mca_refresh: 0.01 <= acq_realtime or 0.1 <= 0.1
25
+ * block_size: 1 <= 1 / acq_realtime <= 1020
26
+ Attention: block_size is not changed if acq_realtime
27
+ is None or nul!
28
+ """
29
+
30
+ params = super().filter_parameters(**kwargs)
31
+
32
+ acq_realtime = params.get("acq_realtime", acq_realtime)
33
+ block_size = params.get("block_size", block_size)
34
+ trigger_mode = params.get("trigger_mode", trigger_mode)
35
+ mca_refresh = params.get("mca_refresh", mca_refresh)
36
+
37
+ if trigger_mode == "INT_TRIG_READOUT":
38
+ params["acq_nb_points"] = 1
39
+
40
+ if mca_refresh is None:
41
+ mca_refresh = np.clip(acq_realtime or 0.1,
42
+ 0.01, 0.1)
43
+ params["mca_refresh"] = float(mca_refresh)
44
+
45
+ if block_size is None and acq_realtime:
46
+ if trigger_mode in ("GATE", "EXT_TRIG"):
47
+ # aiming for a buffer swap every second
48
+ block_size = np.clip(
49
+ 1. / acq_realtime,
50
+ 1, 1020)
51
+ params["block_size"] = int(block_size)
52
+
53
+ return params
54
+
55
+ def set_parameters(self,
56
+ mca_refresh=None,
57
+ **kwargs) -> None:
58
+ super().set_parameters(**kwargs)
59
+ if mca_refresh is not None:
60
+ self.proxy.mca_refresh = mca_refresh
61
+
62
+ @property
63
+ def mca_refresh(self) -> float:
64
+ return self.proxy.mca_refresh
65
+
66
+ @mca_refresh.setter
67
+ def mca_refresh(self, value: float) -> None:
68
+ self.proxy.mca_refresh = value
69
+
70
+
71
+ Mca = FalconX
@@ -0,0 +1,58 @@
1
+ from tango import DeviceProxy
2
+
3
+ from ..utils import is_ds_exported, Callback
4
+
5
+
6
+ class Generator:
7
+ def __init__(self, name, tg_url):
8
+ self._name = name
9
+
10
+ self._tg_url = tg_url
11
+ self._proxy = None
12
+
13
+ @property
14
+ def proxy(self):
15
+ if self._proxy is None:
16
+ if not is_ds_exported(self._tg_url):
17
+ raise RuntimeError(f"Server {self._tg_url} doesn't seem to be running.")
18
+ self._proxy = DeviceProxy(self._tg_url)
19
+ return self._proxy
20
+
21
+ # TODO: put this in bliss
22
+ def __info__(self):
23
+ return f"Generator {self.__class__.__name__} ({self.proxy.name()})"
24
+
25
+ @property
26
+ def acq_nb_points(self):
27
+ return self.proxy.acq_nb_points
28
+
29
+ @acq_nb_points.setter
30
+ def acq_nb_points(self, acq_nb_points):
31
+ self.proxy.acq_nb_points = acq_nb_points
32
+
33
+ @property
34
+ def trigger_mode(self):
35
+ return self.proxy.trigger_mode
36
+
37
+ @trigger_mode.setter
38
+ def trigger_mode(self, trigger_mode):
39
+ self.proxy.trigger_mode = trigger_mode
40
+
41
+ @property
42
+ def acq_realtime(self):
43
+ return self.proxy.acq_realtime
44
+
45
+ @acq_realtime.setter
46
+ def acq_realtime(self, acq_realtime):
47
+ self.proxy.acq_realtime = acq_realtime
48
+
49
+ @property
50
+ def use_external_trigger(self):
51
+ return self.proxy.use_external_trigger
52
+
53
+ @use_external_trigger.setter
54
+ def use_external_trigger(self, use_external_trigger):
55
+ self.proxy.use_external_trigger = use_external_trigger
56
+
57
+ def hw_trig(self):
58
+ self.proxy.hw_trig()
@@ -0,0 +1,9 @@
1
+ from .dante import Dante
2
+ from .simulation import SimulatorMixin
3
+
4
+
5
+ class SimDante(SimulatorMixin, Dante):
6
+ pass
7
+
8
+
9
+ Mca = SimDante
@@ -0,0 +1,9 @@
1
+ from .falconx import FalconX
2
+ from .simulation import SimulatorMixin
3
+
4
+
5
+ class SimFalconX(SimulatorMixin, FalconX):
6
+ pass
7
+
8
+
9
+ Mca = SimFalconX
@@ -0,0 +1,18 @@
1
+ from tango import DeviceProxy
2
+
3
+
4
+ class SimulatorMixin:
5
+ def __init__(self, *args, **kwargs):
6
+ self._generator = None
7
+ super().__init__(*args, **kwargs)
8
+
9
+ def generator(self):
10
+ if self._generator is None:
11
+ # keep everything before "family/instance" as-is, so this
12
+ # also works with a fully-qualified tango://host:port/... URL
13
+ parts = self.tg_url.split("/")
14
+ prefix = "/".join(parts[:-2])
15
+ instance = parts[-1]
16
+ tg_name = f"{prefix}/generator/{instance}"
17
+ self._generator = DeviceProxy(tg_name)
18
+ return self._generator
@@ -0,0 +1,66 @@
1
+ from ..mca import McaBase
2
+ from ..utils import ChangeTangoTimeout
3
+
4
+
5
+ class Xia(McaBase):
6
+
7
+ def __init__(self, *args, **kwargs):
8
+ super().__init__(*args, **kwargs)
9
+
10
+ def stats_mapping(self):
11
+ return {
12
+ # MOSCA : NxWriter
13
+ "output": "events",
14
+ "icr": "icr",
15
+ "ocr": "ocr",
16
+ "livetime": "trigger_livetime",
17
+ "deadtime": "deadtime",
18
+ "realtime": "realtime",
19
+ "triggers": "triggers",
20
+ "livetime_events": "energy_livetime",
21
+ "deadtime_correction": "deadtime_correction",
22
+ }
23
+
24
+ # def _apply_settings(self, ini_file=None, **kwargs):
25
+ # if ini_file:
26
+ # self.configuration_file = ini_file
27
+ # return super()._apply_settings(**kwargs)
28
+
29
+ def __info__(self) -> str:
30
+ txt = super().__info__()
31
+ txt += f"\n * ini file: {self.proxy.ini_file}\n"
32
+ txt += f" * gate ignore: {self.proxy.gate_ignore}\n"
33
+ txt += f" * pix per buf: {self.proxy.pixels_per_buffer}\n"
34
+ txt += f" * preset type: {self.proxy.preset_type}\n"
35
+ txt += f" * preset value: {self.proxy.preset_value}\n"
36
+ return txt
37
+
38
+ @property
39
+ def configuration_file(self) -> str:
40
+ return self.current_configuration()
41
+
42
+ @configuration_file.setter
43
+ def configuration_file(self, fname: str) -> None:
44
+ self.load_configuration(fname)
45
+
46
+ @property
47
+ def configuration_dir(self) -> str:
48
+ return self.proxy.ini_dir
49
+
50
+ def available_configurations(self) -> list[str]:
51
+ return self.proxy.available_ini_files()
52
+
53
+ def _load_configuration(self, configuration: str | None = None) -> None:
54
+ try:
55
+ with ChangeTangoTimeout(self.proxy, 10):
56
+ print(f"Loading {configuration}...")
57
+ if configuration is None:
58
+ configuration = ""
59
+ self.proxy.ini_file = configuration
60
+ except Exception as ex:
61
+ # TODO: report server msg
62
+ raise RuntimeError(f"Loading configuration file '{configuration}' has failed.") from ex
63
+ return self.current_configuration()
64
+
65
+ def current_configuration(self):
66
+ return self.proxy.ini_file
@@ -0,0 +1,96 @@
1
+ from tango import DeviceProxy
2
+
3
+ from .utils import is_ds_exported, Callback
4
+
5
+
6
+ class McaBase:
7
+
8
+ def __init__(self, name, tg_url, on_change_cb=None, **kwargs):
9
+ self._name = name
10
+
11
+ # settings_name = f"mosca:{name}:{self.__class__.__name__}"
12
+ # self._settings2 = HashObjSetting(settings_name)
13
+ self._settings = kwargs.get("settings", {})
14
+
15
+ self._add_setting("configuration", None)
16
+
17
+ self._tg_url = tg_url
18
+ self._proxy = None
19
+ self._on_change_cb = Callback(on_change_cb, blocked=True)
20
+
21
+ @property
22
+ def mca_status(self):
23
+ return self.proxy.mca_status
24
+
25
+ @property
26
+ def tg_url(self):
27
+ return self._tg_url
28
+
29
+ def stats_mapping(self):
30
+ return {}
31
+
32
+ def changed(self):
33
+ self._on_change_cb()
34
+
35
+ def _add_setting(self, key, value=None):
36
+ if key not in self._settings:
37
+ self._settings[key] = value
38
+
39
+ def _set_settings(self, **kwargs):
40
+ for key, value in kwargs.items():
41
+ self._settings[key] = value
42
+
43
+ def initialize(self, load_configuration=True):
44
+ self._on_change_cb.blocked = True
45
+ # iterating on KEYS, because ITEMS doesn't work (returns #ERR)
46
+ settings = {key: (self._settings[key] if load_configuration or key != "configuration" else None)
47
+ for key in self._settings.keys()}
48
+ try:
49
+ self._apply_settings(**settings)
50
+ finally:
51
+ self._on_change_cb.blocked = False
52
+
53
+ def _apply_settings(self, configuration=None, **kwargs):
54
+ if configuration is not None:
55
+ self.load_configuration(configuration)
56
+
57
+ @property
58
+ def proxy(self):
59
+ if self._proxy is None:
60
+ if not is_ds_exported(self._tg_url):
61
+ raise RuntimeError(f"Server {self._tg_url} doesn't seem to be running.")
62
+ self._proxy = DeviceProxy(self._tg_url)
63
+ return self._proxy
64
+
65
+ # TODO: put this in bliss
66
+ def __info__(self):
67
+ return f"MCA {self.__class__.__name__} ({self.proxy.name()})"
68
+
69
+ def load_configuration(self, *args, **kwargs):
70
+ reply = None
71
+ try:
72
+ reply = self._load_configuration(*args, **kwargs)
73
+ except NotImplementedError:
74
+ print(f"MCA doesn't implement load_configuration.")
75
+ else:
76
+ self._set_settings(configuration=reply)
77
+ self.initialize(load_configuration=False)
78
+ self.changed()
79
+ return reply
80
+
81
+ # To be overridden, if necessary
82
+
83
+ def _load_configuration(self, *args, **kwargs):
84
+ raise NotImplementedError
85
+
86
+ def filter_parameters(self, **kwargs):
87
+ return {}
88
+
89
+ def set_parameters(self, **kwargs):
90
+ pass
91
+
92
+ def available_configurations(self, *args, **kwargs):
93
+ return []
94
+
95
+ def current_configuration(self, *args, **kwargs):
96
+ return "N/A"