blissdata-mosca 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: blissdata-mosca
3
+ Version: 2.0.2
4
+ Requires-Python: <3.13,>=3.10
5
+ Requires-Dist: blissdata
6
+ Requires-Dist: h5py
7
+ Requires-Dist: mosca-client
8
+ Requires-Dist: numpy
@@ -0,0 +1,4 @@
1
+ Blissdata Mosca
2
+ ===============
3
+
4
+ Blissdata stream plugin for MOSCA MCA detectors.
@@ -0,0 +1,31 @@
1
+ [project.entry-points.blissdata]
2
+ mosca = "blissdata_mosca"
3
+
4
+ [project]
5
+ name = "blissdata-mosca"
6
+ requires-python = ">=3.10,<3.13"
7
+ version = "2.0.2"
8
+
9
+ dependencies = [
10
+ "numpy",
11
+ "h5py",
12
+ "blissdata",
13
+ "mosca_client"
14
+ ]
15
+
16
+ [build-system]
17
+ build-backend = "hatchling.build"
18
+ requires = ["hatchling"]
19
+
20
+ [tool.pixi.package]
21
+ name = "blissdata-mosca"
22
+ version = { workspace = true }
23
+
24
+ [tool.pixi.package.build]
25
+ backend = { name = "pixi-build-python", version = "0.*" }
26
+
27
+ [tool.pixi.package.host-dependencies]
28
+ hatchling = "==1.26.3"
29
+
30
+ [tool.pixi.pypi-dependencies]
31
+ blissdata_mosca = { path = ".", editable = true }
@@ -0,0 +1,4 @@
1
+ from .stream import MoscaStream, MoscaView
2
+
3
+ stream_cls = MoscaStream
4
+ view_cls = MoscaView
@@ -0,0 +1,297 @@
1
+ import os
2
+ from typing import NamedTuple
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+ import h5py
7
+
8
+ from blissdata.streams import BaseStream, BaseView, StreamDefinition
9
+ from blissdata.streams.encoding.json import JsonStreamEncoder
10
+ from blissdata.exceptions import (
11
+ EndOfStream,
12
+ IndexWontBeThereError,
13
+ IndexNotYetThereError,
14
+ IndexNoMoreThereError,
15
+ EmptyViewException,
16
+ )
17
+
18
+ from mosca_client.tango.mosca import Reader as MoscaReader
19
+
20
+
21
+ PROTOCOL_VERSION = 1
22
+
23
+
24
+ @dataclass
25
+ class SpectraReference:
26
+ file_path: str
27
+ data_path: str | None
28
+ index: int
29
+
30
+
31
+ class SpectraData(NamedTuple):
32
+ array: np.ndarray
33
+ """Numpy array containing data of this frame"""
34
+
35
+ frame_id: int | None
36
+ """Number of the frame (0=first frame) in a sequence of frames."""
37
+
38
+
39
+ class MoscaStream(BaseStream):
40
+ PROTOCOL_VERSION = 1
41
+
42
+ def __init__(self, event_stream):
43
+ super().__init__(event_stream)
44
+
45
+ self._last_read_index = -1
46
+ self._last_saved_index = -1
47
+
48
+ info = event_stream.info
49
+ mosca_info = info["mosca_info"]
50
+
51
+ protocol = mosca_info["protocol_version"]
52
+ if MoscaStream.PROTOCOL_VERSION != protocol:
53
+ raise Exception(
54
+ f"{type(self).__name__} supports mosca json protocol {MoscaStream.PROTOCOL_VERSION}, found version {protocol}"
55
+ )
56
+
57
+ self._points_per_file = mosca_info["points_per_file"]
58
+ self._file_path = mosca_info["file_path"]
59
+ self._file_prefix = mosca_info["file_prefix"]
60
+ self._data_path = mosca_info["data_path"]
61
+ self._dtype = info["dtype"]
62
+ self._shape = info["shape"]
63
+ self._data_index = info["data_index"]
64
+ self._detector = info["detector"]
65
+
66
+ self._reader = MoscaReader(
67
+ event_stream.info["mosca_info"]["server_url"],
68
+ acq_id=event_stream.info["mosca_info"]["acq_id"]
69
+ )
70
+
71
+ @property
72
+ def kind(self):
73
+ return "array"
74
+
75
+ @property
76
+ def dtype(self):
77
+ return self._dtype
78
+
79
+ @property
80
+ def shape(self):
81
+ return self._shape
82
+
83
+ @property
84
+ def data_index(self):
85
+ return self._data_index
86
+
87
+ @staticmethod
88
+ def make_definition(name,
89
+ detector,
90
+ data_index,
91
+ dtype,
92
+ shape,
93
+ tango_url,
94
+ acq_id: int,
95
+ saving={},
96
+ info={}) -> StreamDefinition:
97
+ info = info.copy()
98
+ info["plugin"] = "mosca"
99
+ info["detector"] = detector
100
+ info["data_index"] = data_index
101
+ info["dtype"] = np.dtype(dtype).name
102
+ info["shape"] = shape
103
+ mosca_info = info.setdefault("mosca_info", {})
104
+
105
+ mosca_info["server_url"] = tango_url
106
+ mosca_info["acq_id"] = acq_id
107
+ mosca_info["protocol_version"] = MoscaStream.PROTOCOL_VERSION
108
+
109
+ if saving:
110
+ saving_keys = {
111
+ "file_path",
112
+ "data_path",
113
+ "file_format",
114
+ "file_prefix",
115
+ "points_per_file",
116
+ }
117
+ missing_keys = saving_keys - saving.keys()
118
+ extra_keys = saving.keys() - saving_keys
119
+ if missing_keys:
120
+ raise ValueError(
121
+ f"The following keys are missing from 'saving' dict: {missing_keys}"
122
+ )
123
+ if extra_keys:
124
+ raise ValueError(
125
+ f"The following keys are not expected in 'saving' dict: {extra_keys}"
126
+ )
127
+
128
+ mosca_info.update(saving)
129
+
130
+ return StreamDefinition(name, info, JsonStreamEncoder())
131
+
132
+ @property
133
+ def plugin(self):
134
+ return "mosca"
135
+
136
+ def __len__(self):
137
+ return self._reader.n_available_points
138
+
139
+ def __getitem__(self, key):
140
+ if self._last_read_index == -1:
141
+ self._update()
142
+
143
+ stop = None
144
+ if isinstance(key, int):
145
+ if key == -1:
146
+ return self.get_last_live_spectra()
147
+ start = key
148
+ stop = key + 1
149
+ elif isinstance(key, slice):
150
+ if key.step not in (1, None):
151
+ raise RuntimeError(f"slice.step not in (1, None)")
152
+ start = 0 if key.start is None else key.start
153
+ stop = self._last_read_index + 1 if key.stop in (-1, None) else key.stop
154
+ else:
155
+ raise TypeError(f"Invalid type for arg key: {type(key)}.")
156
+
157
+ data = self._read(start, stop)
158
+ return data
159
+
160
+ def _update(self):
161
+ last_event = self.event_stream[-1]
162
+ self._last_read_index = last_event["last_read_index"]
163
+ self._last_saved_index = last_event["last_saved_index"]
164
+
165
+ def _read(self, start, stop):
166
+ if stop - 1 <= self._last_saved_index:
167
+ data = self._get_from_files(start, stop)
168
+ elif start < self._last_saved_index:
169
+ file_data = self._get_from_files(start, self._last_saved_index + 1)
170
+ server_data = self._reader.read_spectra(self._last_saved_index + 1, stop)[self.data_index]
171
+ data = np.concatenate((file_data, server_data), axis=0)
172
+ else:
173
+ data = self._reader.read_spectra(start, stop)[self.data_index]
174
+ return data
175
+
176
+ def _get_from_files(self, start, stop):
177
+ n_points = stop - start
178
+
179
+ data = np.zeros((n_points, self.shape[-1]), self.dtype)
180
+
181
+ assert start < stop
182
+ data_path = f"{self._data_path}/spectra"
183
+
184
+ filenames, dset_idx = self._idx_to_files(start, stop)
185
+
186
+ data_idx = np.add.accumulate(np.diff(dset_idx, axis=1)[:, 0])
187
+ data_idx = np.concatenate(([0], data_idx))
188
+
189
+ for f_idx, filename in enumerate(filenames):
190
+ dset_0, dset_1 = dset_idx[f_idx]
191
+ data_0, data_1 = data_idx[f_idx:f_idx + 2]
192
+ with h5py.File(filename, "r") as h5f:
193
+ data[data_0:data_1, :] = h5f[data_path][self.data_index, dset_0:dset_1, :]
194
+
195
+ return data
196
+
197
+ def _idx_to_files(self, start, stop):
198
+ """ Given a range of points [start .. stop[, returns
199
+ a list of files containing those points,
200
+ along with the indices of those points within the files.
201
+
202
+ Returns three objects:
203
+ - ordered list of N file names
204
+ - ordered list of N 2-tuples, dataset indices range within each file """
205
+
206
+ assert start < stop
207
+ assert start >= 0
208
+
209
+ ppf = self._points_per_file
210
+ basename = os.path.join(self._file_path, f"{self._file_prefix}spectra_")
211
+
212
+ # file indices, points interval within the returned data
213
+ file_idx, data_idx = np.unique(np.arange(start, stop) // ppf,
214
+ return_index=True)
215
+ filenames = [f"{basename}{f_idx:04d}.h5" for f_idx in file_idx]
216
+
217
+ n_points = stop - start
218
+ dset_idx = np.zeros((len(filenames), 2), dtype=int)
219
+
220
+ def _last(n):
221
+ if n >= ppf:
222
+ return ppf
223
+ return n
224
+
225
+ dset_idx[0][:] = [start % ppf, _last((start % ppf) + n_points)]
226
+ for i, idx in enumerate(data_idx[1:], start=1):
227
+ dset_idx[i][:] = [0, _last(n_points - idx)]
228
+
229
+ return filenames, dset_idx
230
+
231
+ def get_references(self, start, stop):
232
+ if self._last_read_index == -1:
233
+ self._update()
234
+
235
+ data_path = f"{self._data_path}/spectra_{self._detector:02d}"
236
+ files, dset_idx = self._idx_to_files(start, stop)
237
+
238
+ references = [SpectraReference(file, data_path, idx)
239
+ for file, indices in zip(files, dset_idx)
240
+ for idx in range(indices[0], indices[1])]
241
+
242
+ return references
243
+
244
+ def _need_last_only(self, last_only):
245
+ # mosca uses json stream as a status,
246
+ # last one is the only valuable status
247
+ # forcing True
248
+ return True
249
+
250
+ def _build_view_from_events(self, index, events, last_only):
251
+ # print("===> EVENT", index, events, last_only)
252
+ lri = events.data[-1]["last_read_index"]
253
+ lsi = events.data[-1]["last_saved_index"]
254
+ # n_spec = len(self)
255
+ if lri < index:
256
+ raise EmptyViewException
257
+
258
+ if last_only:
259
+ start = lri
260
+ else:
261
+ start = index
262
+ self._last_read_index = lri
263
+ self._last_saved_index = lsi
264
+ return MoscaView(self, start, lri + 1)
265
+
266
+ def get_last_live_spectra(self):
267
+ print("R1", self.data_index)
268
+ if self._last_read_index == -1:
269
+ self._update()
270
+ index = self._last_read_index
271
+ data = self._reader.read_spectra(index, self._last_read_index + 1)[self.data_index, 0]
272
+ return SpectraData(array=data, frame_id=index)
273
+
274
+
275
+ class MoscaView(BaseView):
276
+ def __init__(self, stream, start, stop):
277
+ self._client = stream
278
+ self._view_range = range(start, stop)
279
+
280
+ @property
281
+ def index(self):
282
+ return self._view_range.start
283
+
284
+ def __len__(self):
285
+ return len(self._view_range)
286
+
287
+ def get_data(self, start=None, stop=None):
288
+ trimmed_range = self._view_range[start:stop]
289
+ return self._client[trimmed_range.start : trimmed_range.stop]
290
+
291
+ def get_references(
292
+ self, start=None, stop=None
293
+ ) -> SpectraReference | list[SpectraReference]:
294
+ trimmed_range = self._view_range[start:stop]
295
+ return self._client.get_references(
296
+ trimmed_range.start, trimmed_range.stop
297
+ )