pymmcore-plus 0.15.3__py3-none-any.whl → 0.16.0__py3-none-any.whl
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.
- pymmcore_plus/__init__.py +20 -1
- pymmcore_plus/_accumulator.py +23 -5
- pymmcore_plus/_cli.py +44 -26
- pymmcore_plus/_discovery.py +344 -0
- pymmcore_plus/_logger.py +1 -1
- pymmcore_plus/_util.py +9 -245
- pymmcore_plus/core/_device.py +20 -7
- pymmcore_plus/core/_mmcore_plus.py +16 -8
- pymmcore_plus/core/_property.py +34 -28
- pymmcore_plus/core/events/_device_signal_view.py +8 -1
- pymmcore_plus/core/events/_protocol.py +3 -0
- pymmcore_plus/core/events/_psygnal.py +1 -0
- pymmcore_plus/core/events/_qsignals.py +1 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +20 -13
- pymmcore_plus/experimental/unicore/core/_unicore.py +4 -1
- pymmcore_plus/install.py +150 -18
- pymmcore_plus/mda/_engine.py +268 -73
- pymmcore_plus/mda/events/__init__.py +1 -1
- pymmcore_plus/metadata/_ome.py +499 -0
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/METADATA +3 -2
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/RECORD +24 -22
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -26,7 +26,7 @@ def _get_auto_MDA_callback_class() -> type[PMDASignaler]:
|
|
|
26
26
|
return QMDASignaler
|
|
27
27
|
|
|
28
28
|
# (not sure why this type ignore is needed... apparently isn't matching protocol)
|
|
29
|
-
return MDASignaler
|
|
29
|
+
return MDASignaler
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def __dir__() -> list[str]: # pragma: no cover
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
7
|
+
|
|
8
|
+
import useq
|
|
9
|
+
from ome_types.model import (
|
|
10
|
+
OME,
|
|
11
|
+
Channel,
|
|
12
|
+
Image,
|
|
13
|
+
ImageRef,
|
|
14
|
+
Instrument,
|
|
15
|
+
Pixels,
|
|
16
|
+
Pixels_DimensionOrder,
|
|
17
|
+
PixelType,
|
|
18
|
+
Plane,
|
|
19
|
+
Plate,
|
|
20
|
+
UnitsLength,
|
|
21
|
+
UnitsTime,
|
|
22
|
+
Well,
|
|
23
|
+
WellSample,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from pymmcore_plus.mda._runner import GeneratorMDASequence
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from pymmcore_plus.metadata.schema import ImageInfo
|
|
30
|
+
|
|
31
|
+
from .schema import FrameMetaV1, SummaryMetaV1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["create_ome_metadata"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_ome_metadata(
|
|
38
|
+
summary_metadata: SummaryMetaV1, frame_metadata_list: list[FrameMetaV1]
|
|
39
|
+
) -> OME:
|
|
40
|
+
"""Create OME metadata from metadata saved as json by the core engine.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
summary_metadata : SummaryMetaV1
|
|
45
|
+
Summary metadata containing acquisition information.
|
|
46
|
+
frame_metadata_list : list[FrameMetaV1]
|
|
47
|
+
List of frame metadata for each acquired image.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
OME
|
|
52
|
+
The OME metadata as an `ome_types.OME` object.
|
|
53
|
+
"""
|
|
54
|
+
_uuid = f"urn:uuid:{uuid.uuid4()}"
|
|
55
|
+
ome = OME(uuid=_uuid)
|
|
56
|
+
|
|
57
|
+
ome.instruments = instruments = _build_instrument_list(summary_metadata)
|
|
58
|
+
|
|
59
|
+
image_infos = summary_metadata.get("image_infos", ())
|
|
60
|
+
if not frame_metadata_list or not image_infos:
|
|
61
|
+
return ome
|
|
62
|
+
|
|
63
|
+
sequence = _extract_mda_sequence(summary_metadata, frame_metadata_list[0])
|
|
64
|
+
position_groups = _group_frames_by_position(frame_metadata_list)
|
|
65
|
+
images = _build_ome_images(
|
|
66
|
+
dimension_info=_extract_dimension_info(image_infos[0]),
|
|
67
|
+
sequence=sequence,
|
|
68
|
+
position_groups=position_groups,
|
|
69
|
+
acquisition_date=_extract_acquisition_date(summary_metadata),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
plates = []
|
|
73
|
+
if (plate_plan := _extract_plate_plan(sequence)) is not None:
|
|
74
|
+
position_to_image_mapping = _create_position_to_image_mapping(position_groups)
|
|
75
|
+
plates = [_build_ome_plate(plate_plan, position_to_image_mapping)]
|
|
76
|
+
|
|
77
|
+
return OME(
|
|
78
|
+
uuid=_uuid,
|
|
79
|
+
images=images,
|
|
80
|
+
instruments=instruments,
|
|
81
|
+
plates=plates,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# Data Structures
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class _DimensionInfo(NamedTuple):
|
|
91
|
+
pixel_size_um: float
|
|
92
|
+
dtype: str | None
|
|
93
|
+
height: int
|
|
94
|
+
width: int
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _PositionKey(NamedTuple):
|
|
98
|
+
name: str | None
|
|
99
|
+
p_index: int
|
|
100
|
+
g_index: int | None = None
|
|
101
|
+
|
|
102
|
+
def __str__(self) -> str:
|
|
103
|
+
p_name = self.name or f"Pos{self.p_index:04d}"
|
|
104
|
+
if self.g_index is not None:
|
|
105
|
+
return f"{p_name}_Grid{self.g_index:04d}_{self.p_index}"
|
|
106
|
+
else:
|
|
107
|
+
return f"{p_name}_{self.p_index}"
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def image_id(self) -> str:
|
|
111
|
+
if self.g_index is not None:
|
|
112
|
+
return f"{self.p_index}_{self.g_index}"
|
|
113
|
+
return f"{self.p_index}"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# =============================================================================
|
|
117
|
+
# Metadata Extraction Functions
|
|
118
|
+
# =============================================================================
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _extract_dimension_info(
|
|
122
|
+
image_info: ImageInfo,
|
|
123
|
+
) -> _DimensionInfo:
|
|
124
|
+
"""Extract pixel size (µm), data type, width, and height from image_infos."""
|
|
125
|
+
return _DimensionInfo(
|
|
126
|
+
pixel_size_um=image_info.get("pixel_size_um", 1.0),
|
|
127
|
+
dtype=image_info.get("dtype", None),
|
|
128
|
+
width=image_info.get("width", 0),
|
|
129
|
+
height=image_info.get("height", 0),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _extract_acquisition_date(summary_metadata: SummaryMetaV1) -> datetime | None:
|
|
134
|
+
"""Extract acquisition date from summary metadata."""
|
|
135
|
+
if (acquisition_time := summary_metadata.get("datetime")) is not None:
|
|
136
|
+
with suppress(ValueError, AttributeError):
|
|
137
|
+
return datetime.fromisoformat(acquisition_time.replace("Z", "+00:00"))
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _extract_mda_sequence(
|
|
142
|
+
summary_metadata: SummaryMetaV1, single_frame_metadata: FrameMetaV1
|
|
143
|
+
) -> useq.MDASequence | None:
|
|
144
|
+
"""Extract the MDA sequence from summary metadata or frame metadata."""
|
|
145
|
+
if (sequence_data := summary_metadata.get("mda_sequence")) is not None:
|
|
146
|
+
return useq.MDASequence.model_validate(sequence_data)
|
|
147
|
+
if (mda_event := _extract_mda_event(single_frame_metadata)) is not None:
|
|
148
|
+
return mda_event.sequence
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _extract_mda_event(frame_metadata: FrameMetaV1) -> useq.MDAEvent | None:
|
|
153
|
+
"""Extract the useq.MDAEvent from frame metadata."""
|
|
154
|
+
if (mda_event_data := frame_metadata.get("mda_event")) is not None:
|
|
155
|
+
return useq.MDAEvent.model_validate(mda_event_data)
|
|
156
|
+
return None # pragma: no cover
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _extract_plate_plan(
|
|
160
|
+
sequence: useq.MDASequence | None,
|
|
161
|
+
) -> useq.WellPlatePlan | None:
|
|
162
|
+
"""Extract the plate plan from the MDA sequence if it exists."""
|
|
163
|
+
if sequence is None: # pragma: no cover
|
|
164
|
+
return None
|
|
165
|
+
stage_positions = sequence.stage_positions
|
|
166
|
+
if isinstance(stage_positions, useq.WellPlatePlan):
|
|
167
|
+
return stage_positions
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# =============================================================================
|
|
172
|
+
# Frame Grouping and Processing
|
|
173
|
+
# =============================================================================
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _group_frames_by_position(
|
|
177
|
+
frame_metadata_list: list[FrameMetaV1],
|
|
178
|
+
) -> dict[_PositionKey, list[FrameMetaV1]]:
|
|
179
|
+
"""Reorganize frame metadata by stage position index in a dictionary.
|
|
180
|
+
|
|
181
|
+
Handles the 'g' axis (grid) by converting it to separate positions,
|
|
182
|
+
since OME doesn't support the 'g' axis. Each grid position becomes
|
|
183
|
+
a separate OME Image with names like "Pos0000_Grid0000".
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
dict[str, list[FrameMetaV1]]
|
|
188
|
+
mapping of position identifier (e.g. 'Pos0000_Grid0000')
|
|
189
|
+
to list of `FrameMetaV1`.
|
|
190
|
+
"""
|
|
191
|
+
frames_by_position: dict[_PositionKey, list[FrameMetaV1]] = {}
|
|
192
|
+
for frame_metadata in frame_metadata_list:
|
|
193
|
+
if (mda_event := _extract_mda_event(frame_metadata)) is None:
|
|
194
|
+
continue # pragma: no cover
|
|
195
|
+
|
|
196
|
+
p_index = mda_event.index.get(useq.Axis.POSITION, 0) or 0
|
|
197
|
+
g_index = mda_event.index.get(useq.Axis.GRID, None)
|
|
198
|
+
key = _PositionKey(mda_event.pos_name, p_index, g_index)
|
|
199
|
+
pos_list = frames_by_position.setdefault(key, [])
|
|
200
|
+
pos_list.append(frame_metadata)
|
|
201
|
+
return frames_by_position
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _create_position_to_image_mapping(
|
|
205
|
+
position_groups: dict[_PositionKey, list[FrameMetaV1]],
|
|
206
|
+
) -> dict[int, str]:
|
|
207
|
+
"""Create a mapping from position index to image ID."""
|
|
208
|
+
position_to_image_mapping: dict[int, str] = {}
|
|
209
|
+
|
|
210
|
+
for position_key, position_frames in position_groups.items():
|
|
211
|
+
if position_frames:
|
|
212
|
+
mda_event = _extract_mda_event(position_frames[0])
|
|
213
|
+
if mda_event is not None:
|
|
214
|
+
position_index = mda_event.index.get("p", 0)
|
|
215
|
+
position_to_image_mapping[position_index] = position_key.image_id
|
|
216
|
+
return position_to_image_mapping
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# =============================================================================
|
|
220
|
+
# Dimension Order and Pixel Information
|
|
221
|
+
# =============================================================================
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _determine_dimension_order(
|
|
225
|
+
sequence: useq.MDASequence | None,
|
|
226
|
+
) -> Pixels_DimensionOrder | None:
|
|
227
|
+
"""Determine the dimension order for pixels."""
|
|
228
|
+
if sequence is None or isinstance(sequence, GeneratorMDASequence):
|
|
229
|
+
return Pixels_DimensionOrder.XYTCZ
|
|
230
|
+
return _extract_dimension_order_from_sequence(sequence)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _extract_dimension_order_from_sequence(
|
|
234
|
+
sequence: useq.MDASequence,
|
|
235
|
+
) -> Pixels_DimensionOrder:
|
|
236
|
+
"""Extract axis order from a useq.MDASequence.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
A Pixels_DimensionOrder representing the dimension order compatible with OME
|
|
241
|
+
standards
|
|
242
|
+
(e.g., "XYCZT").
|
|
243
|
+
"""
|
|
244
|
+
filtered_axes = (axis for axis in sequence.axis_order if axis not in {"p", "g"})
|
|
245
|
+
dimension_order = "XY" + "".join(filtered_axes).upper()
|
|
246
|
+
|
|
247
|
+
if len(dimension_order) != 5:
|
|
248
|
+
missing_axes = [axis for axis in "XYCZT" if axis not in dimension_order]
|
|
249
|
+
dimension_order += "".join(missing_axes)
|
|
250
|
+
|
|
251
|
+
return Pixels_DimensionOrder(dimension_order)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _extract_pixel_dimensions_and_channels(
|
|
255
|
+
sequence: useq.MDASequence | None,
|
|
256
|
+
position_frames: list[FrameMetaV1],
|
|
257
|
+
image_id: str,
|
|
258
|
+
) -> tuple[tuple[int, int, int], list[Channel]]:
|
|
259
|
+
"""Extract pixel dimensions and channels from sequence or frames."""
|
|
260
|
+
if sequence is None or isinstance(sequence, GeneratorMDASequence):
|
|
261
|
+
return _extract_pixel_info_from_frames(position_frames, image_id)
|
|
262
|
+
return _extract_pixel_info_from_sequence(sequence, image_id)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _extract_pixel_info_from_frames(
|
|
266
|
+
position_metadata: list[FrameMetaV1],
|
|
267
|
+
image_id: str,
|
|
268
|
+
) -> tuple[tuple[int, int, int], list[Channel]]:
|
|
269
|
+
"""Extract pixel dimensions and channel information from frame metadata.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
A tuple containing the maximum (t, z, c) dimensions, and a list of channels.
|
|
274
|
+
"""
|
|
275
|
+
max_t, max_z, max_c = 0, 0, 0
|
|
276
|
+
channels: dict[int, Channel] = {}
|
|
277
|
+
|
|
278
|
+
for frame_metadata in position_metadata:
|
|
279
|
+
mda_event = _extract_mda_event(frame_metadata)
|
|
280
|
+
if mda_event is None: # pragma: no cover
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
t_index = mda_event.index.get("t", 0)
|
|
284
|
+
z_index = mda_event.index.get("z", 0)
|
|
285
|
+
c_index = mda_event.index.get("c", 0)
|
|
286
|
+
|
|
287
|
+
max_t = max(max_t, t_index)
|
|
288
|
+
max_z = max(max_z, z_index)
|
|
289
|
+
max_c = max(max_c, c_index)
|
|
290
|
+
|
|
291
|
+
if c_index not in channels and mda_event.channel is not None:
|
|
292
|
+
channels[c_index] = Channel(
|
|
293
|
+
id=f"Channel:{image_id}:{c_index}",
|
|
294
|
+
name=mda_event.channel.config,
|
|
295
|
+
samples_per_pixel=1,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
sorted_channels = [channels[i] for i in sorted(channels.keys())]
|
|
299
|
+
return (max_t + 1, max_z + 1, max_c + 1), sorted_channels
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _extract_pixel_info_from_sequence(
|
|
303
|
+
sequence: useq.MDASequence,
|
|
304
|
+
image_id: str,
|
|
305
|
+
) -> tuple[tuple[int, int, int], list[Channel]]:
|
|
306
|
+
"""Extract pixel dimensions and channel information from MDA sequence."""
|
|
307
|
+
max_t = sequence.sizes.get("t", 1)
|
|
308
|
+
max_z = sequence.sizes.get("z", 1)
|
|
309
|
+
channels = [
|
|
310
|
+
Channel(
|
|
311
|
+
id=f"Channel:{image_id}:{index}",
|
|
312
|
+
name=channel.config,
|
|
313
|
+
samples_per_pixel=1,
|
|
314
|
+
)
|
|
315
|
+
for index, channel in enumerate(sequence.channels)
|
|
316
|
+
]
|
|
317
|
+
return (max_t, max_z, len(channels)), channels
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# =============================================================================
|
|
321
|
+
# OME Object Builders
|
|
322
|
+
# =============================================================================
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _build_ome_images(
|
|
326
|
+
dimension_info: _DimensionInfo,
|
|
327
|
+
sequence: useq.MDASequence | None,
|
|
328
|
+
position_groups: dict[_PositionKey, list[FrameMetaV1]],
|
|
329
|
+
acquisition_date: datetime | None,
|
|
330
|
+
) -> list[Image]:
|
|
331
|
+
"""Build OME Images from grouped frame metadata by position."""
|
|
332
|
+
images = []
|
|
333
|
+
for position_key, position_frames in position_groups.items():
|
|
334
|
+
image_id = position_key.image_id
|
|
335
|
+
position_name = str(position_key)
|
|
336
|
+
|
|
337
|
+
dimension_order = _determine_dimension_order(sequence)
|
|
338
|
+
if not dimension_order: # pragma: no cover
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
size_info, channels = _extract_pixel_dimensions_and_channels(
|
|
342
|
+
sequence, position_frames, image_id
|
|
343
|
+
)
|
|
344
|
+
max_t, max_z, max_c = size_info
|
|
345
|
+
|
|
346
|
+
pixels = _build_pixels_object(
|
|
347
|
+
image_id,
|
|
348
|
+
dimension_order,
|
|
349
|
+
dimension_info,
|
|
350
|
+
max_t,
|
|
351
|
+
max_z,
|
|
352
|
+
max_c,
|
|
353
|
+
channels,
|
|
354
|
+
position_frames,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
image = Image(
|
|
358
|
+
acquisition_date=acquisition_date,
|
|
359
|
+
id=f"Image:{image_id}",
|
|
360
|
+
name=position_name,
|
|
361
|
+
pixels=pixels,
|
|
362
|
+
)
|
|
363
|
+
images.append(image)
|
|
364
|
+
return images
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _build_pixels_object(
|
|
368
|
+
image_id: str,
|
|
369
|
+
dimension_order: Pixels_DimensionOrder,
|
|
370
|
+
dimension_info: _DimensionInfo,
|
|
371
|
+
max_t: int,
|
|
372
|
+
max_z: int,
|
|
373
|
+
max_c: int,
|
|
374
|
+
channels: list[Channel],
|
|
375
|
+
position_frames: list[FrameMetaV1],
|
|
376
|
+
) -> Pixels:
|
|
377
|
+
"""Build a Pixels object with the given parameters."""
|
|
378
|
+
from ome_types.model import MetadataOnly
|
|
379
|
+
|
|
380
|
+
return Pixels(
|
|
381
|
+
id=f"Pixels:{image_id}",
|
|
382
|
+
dimension_order=dimension_order,
|
|
383
|
+
size_x=dimension_info.width,
|
|
384
|
+
size_y=dimension_info.height,
|
|
385
|
+
size_z=max(max_z, 1),
|
|
386
|
+
size_c=max(max_c, 1),
|
|
387
|
+
size_t=max(max_t, 1),
|
|
388
|
+
type=PixelType(dimension_info.dtype),
|
|
389
|
+
physical_size_x=dimension_info.pixel_size_um,
|
|
390
|
+
physical_size_x_unit=UnitsLength.MICROMETER,
|
|
391
|
+
physical_size_y=dimension_info.pixel_size_um,
|
|
392
|
+
physical_size_y_unit=UnitsLength.MICROMETER,
|
|
393
|
+
channels=channels,
|
|
394
|
+
metadata_only=MetadataOnly(),
|
|
395
|
+
planes=_build_plane_list(position_frames),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _build_plane_list(position_frames: list[FrameMetaV1]) -> list[Plane]:
|
|
400
|
+
"""Build Plane objects for frame metadata at a specific position."""
|
|
401
|
+
planes = []
|
|
402
|
+
for frame_metadata in position_frames:
|
|
403
|
+
mda_event = _extract_mda_event(frame_metadata)
|
|
404
|
+
if mda_event is None: # pragma: no cover
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
event_index = mda_event.index
|
|
408
|
+
z_index = event_index.get("z", 0)
|
|
409
|
+
c_index = event_index.get("c", 0)
|
|
410
|
+
t_index = event_index.get("t", 0)
|
|
411
|
+
|
|
412
|
+
runner_time_ms = frame_metadata.get("runner_time_ms", 0.0)
|
|
413
|
+
delta_t = runner_time_ms / 1000.0 if runner_time_ms > 0 else None
|
|
414
|
+
exposure_ms = frame_metadata.get("exposure_ms", 0.0)
|
|
415
|
+
|
|
416
|
+
plane = Plane(
|
|
417
|
+
the_z=z_index,
|
|
418
|
+
the_c=c_index,
|
|
419
|
+
the_t=t_index,
|
|
420
|
+
position_x=mda_event.x_pos,
|
|
421
|
+
position_x_unit=UnitsLength.MICROMETER,
|
|
422
|
+
position_y=mda_event.y_pos,
|
|
423
|
+
position_y_unit=UnitsLength.MICROMETER,
|
|
424
|
+
position_z=mda_event.z_pos,
|
|
425
|
+
position_z_unit=UnitsLength.MICROMETER,
|
|
426
|
+
delta_t=delta_t,
|
|
427
|
+
delta_t_unit=UnitsTime.SECOND,
|
|
428
|
+
exposure_time=exposure_ms,
|
|
429
|
+
exposure_time_unit=UnitsTime.MILLISECOND,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
planes.append(plane)
|
|
433
|
+
return planes
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _build_ome_plate(
|
|
437
|
+
plate_plan: useq.WellPlatePlan, position_to_image_mapping: dict[int, str]
|
|
438
|
+
) -> Plate:
|
|
439
|
+
"""Create a Plate object from a useq.WellPlatePlan."""
|
|
440
|
+
wells: list[Well] = []
|
|
441
|
+
|
|
442
|
+
# create a mapping from well name to acquisition indices
|
|
443
|
+
well_acquisition_map: dict[str, list[int]] = {}
|
|
444
|
+
for acquisition_index, position in enumerate(plate_plan.image_positions):
|
|
445
|
+
well_name = position.name
|
|
446
|
+
if well_name is not None:
|
|
447
|
+
if well_name not in well_acquisition_map:
|
|
448
|
+
well_acquisition_map[well_name] = []
|
|
449
|
+
well_acquisition_map[well_name].append(acquisition_index)
|
|
450
|
+
|
|
451
|
+
for (row, col), name, pos in zip(
|
|
452
|
+
plate_plan.selected_well_indices,
|
|
453
|
+
plate_plan.selected_well_names,
|
|
454
|
+
plate_plan.selected_well_positions,
|
|
455
|
+
):
|
|
456
|
+
# get all acquisition indices for this well
|
|
457
|
+
acquisition_indices = well_acquisition_map.get(name, [])
|
|
458
|
+
|
|
459
|
+
# create WellSample objects for each acquisition in this well
|
|
460
|
+
well_samples = []
|
|
461
|
+
for acq_index in acquisition_indices:
|
|
462
|
+
# Use the actual image ID from the mapping
|
|
463
|
+
image_id = position_to_image_mapping.get(acq_index, str(acq_index))
|
|
464
|
+
well_samples.append(
|
|
465
|
+
WellSample(
|
|
466
|
+
id=f"WellSample:{acq_index}",
|
|
467
|
+
position_x=pos.x,
|
|
468
|
+
position_y=pos.y,
|
|
469
|
+
position_x_unit=UnitsLength.MICROMETER,
|
|
470
|
+
position_y_unit=UnitsLength.MICROMETER,
|
|
471
|
+
index=acq_index,
|
|
472
|
+
image_ref=ImageRef(id=f"Image:{image_id}"),
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
wells.append(
|
|
477
|
+
Well(
|
|
478
|
+
row=row,
|
|
479
|
+
column=col,
|
|
480
|
+
well_samples=well_samples,
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
return Plate(
|
|
485
|
+
name=plate_plan.plate.name,
|
|
486
|
+
rows=plate_plan.plate.rows,
|
|
487
|
+
columns=plate_plan.plate.columns,
|
|
488
|
+
wells=wells,
|
|
489
|
+
well_origin_x=plate_plan.a1_center_xy[0],
|
|
490
|
+
well_origin_x_unit=UnitsLength.MICROMETER,
|
|
491
|
+
well_origin_y=plate_plan.a1_center_xy[1],
|
|
492
|
+
well_origin_y_unit=UnitsLength.MICROMETER,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _build_instrument_list(summary_metadata: SummaryMetaV1) -> list[Instrument]:
|
|
497
|
+
"""Build instrument list from summary metadata."""
|
|
498
|
+
# TODO
|
|
499
|
+
return []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymmcore-plus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: pymmcore superset providing improved APIs, event handling, and a pure python acquisition engine
|
|
5
5
|
Project-URL: Source, https://github.com/pymmcore-plus/pymmcore-plus
|
|
6
6
|
Project-URL: Tracker, https://github.com/pymmcore-plus/pymmcore-plus/issues
|
|
@@ -28,9 +28,10 @@ Requires-Python: >=3.9
|
|
|
28
28
|
Requires-Dist: numpy>=1.25.2
|
|
29
29
|
Requires-Dist: numpy>=1.26.0; python_version >= '3.12'
|
|
30
30
|
Requires-Dist: numpy>=2.1.0; python_version >= '3.13'
|
|
31
|
+
Requires-Dist: ome-types>=0.6.0
|
|
31
32
|
Requires-Dist: platformdirs>=3.0.0
|
|
32
33
|
Requires-Dist: psygnal>=0.10
|
|
33
|
-
Requires-Dist: pymmcore>=11.
|
|
34
|
+
Requires-Dist: pymmcore>=11.10.0.74.0
|
|
34
35
|
Requires-Dist: rich>=10.2.0
|
|
35
36
|
Requires-Dist: tensorstore!=0.1.72,>=0.1.67
|
|
36
37
|
Requires-Dist: tensorstore!=0.1.72,>=0.1.71; python_version >= '3.13'
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
pymmcore_plus/__init__.py,sha256=
|
|
2
|
-
pymmcore_plus/_accumulator.py,sha256=
|
|
1
|
+
pymmcore_plus/__init__.py,sha256=Z5CdLIfBVp1Oe-J-uW2VBzjBFas3GPMRea8OSPJYePM,3030
|
|
2
|
+
pymmcore_plus/_accumulator.py,sha256=kQzVAt-lS6DAokJz-nbB7oQBxb23bjT46kCiM_y7rJE,9853
|
|
3
3
|
pymmcore_plus/_benchmark.py,sha256=YJICxXleFQVbOluJdq4OujnIcTkkuMVzeB8GJ8nUv5I,6011
|
|
4
4
|
pymmcore_plus/_build.py,sha256=RPTAuwCZWGL5IDJj4JZo1DIIouUsIqS3vnbPbG2_bRE,10993
|
|
5
|
-
pymmcore_plus/_cli.py,sha256=
|
|
5
|
+
pymmcore_plus/_cli.py,sha256=vjF02cLFt0hIJHN5jwhi7KKgqjsFJCE0Wihfg2_x7KQ,17410
|
|
6
|
+
pymmcore_plus/_discovery.py,sha256=rBsf_-H5nQ6MUOD8wPV0c0iOc46nU2RszmUxc7iziVs,12785
|
|
6
7
|
pymmcore_plus/_ipy_completion.py,sha256=K__bKTJZukzz0RhNr3KZ2urpQGxQrW8ySUQtHDoKfLA,17936
|
|
7
|
-
pymmcore_plus/_logger.py,sha256=
|
|
8
|
+
pymmcore_plus/_logger.py,sha256=Fzwk8k6zvkhMKajx_vrVjbR-D2o6mKtYivG2Jgyflmc,5421
|
|
8
9
|
pymmcore_plus/_pymmcore.py,sha256=hy0gfEhvQ84o7SFIB2nQp6A1w2L4U7VQrV5_qKsJoLQ,699
|
|
9
|
-
pymmcore_plus/_util.py,sha256=
|
|
10
|
-
pymmcore_plus/install.py,sha256=
|
|
10
|
+
pymmcore_plus/_util.py,sha256=xskovMeMbFDpKcqEv-bADiwm37f-AmPpaPfTPPBjyyo,14278
|
|
11
|
+
pymmcore_plus/install.py,sha256=a7MqkqJFevXgouvLNflypCu7-Y9oe1KKOwImwhyIaO8,16000
|
|
11
12
|
pymmcore_plus/mocks.py,sha256=jNUfmffD1OArtIvEmqWsy7GCrtTpssVF03flH8cEYx8,1867
|
|
12
13
|
pymmcore_plus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
14
|
pymmcore_plus/seq_tester.py,sha256=ielLx2ZUJrOXVCojk64UXTeKDoARxt8QkQjt5AE5Gng,3776
|
|
@@ -16,26 +17,26 @@ pymmcore_plus/core/_adapter.py,sha256=eu2BhGe_dnoQrIsh-u3poxWXsiF2Y8pfbKIGWbUgOk
|
|
|
16
17
|
pymmcore_plus/core/_config.py,sha256=yWwOnW6f37lLt83MnodNce04az-g8YDjyo7BvMiTc8s,10672
|
|
17
18
|
pymmcore_plus/core/_config_group.py,sha256=R-o4xuPDBPQAC3s-mFsiKwHVKWR38L9qq_aoWdPrAq8,8542
|
|
18
19
|
pymmcore_plus/core/_constants.py,sha256=tpXCn4CdhevGabUwOSvn7klMRoiXiVPmsPlAofL6Ai4,14122
|
|
19
|
-
pymmcore_plus/core/_device.py,sha256=
|
|
20
|
+
pymmcore_plus/core/_device.py,sha256=x-fZcWu7KGScbxj5CPQomdFMYkvnFWJXGUJuYEfcYNc,33623
|
|
20
21
|
pymmcore_plus/core/_metadata.py,sha256=L8x1gX_zXPz02BUqc7eqJM_Bey2G0RyX30SOBs2aBNc,2755
|
|
21
|
-
pymmcore_plus/core/_mmcore_plus.py,sha256=
|
|
22
|
-
pymmcore_plus/core/_property.py,sha256=
|
|
22
|
+
pymmcore_plus/core/_mmcore_plus.py,sha256=3Ij6wWpTm2vcYKwdXY29pRKdrTkTd60va4hZMxAEU7c,103150
|
|
23
|
+
pymmcore_plus/core/_property.py,sha256=fiqxke6Y_3wsXrDTzbGJ4jioj_M-syWPL-7fufUPNow,8514
|
|
23
24
|
pymmcore_plus/core/_sequencing.py,sha256=pOydKOsHjQOlYGp51hrSjHz02s4NoSEzQwTTSh3R4_Q,16887
|
|
24
25
|
pymmcore_plus/core/events/__init__.py,sha256=F8r10LEBLrAV8qfkXScSkpqfExdT2XoOx92OqSturpc,1078
|
|
25
26
|
pymmcore_plus/core/events/_deprecated.py,sha256=H_Sd63ZyV3Hkq8Imozi8OE7FIa_hhSRpEyhjLj4xTB8,2241
|
|
26
|
-
pymmcore_plus/core/events/_device_signal_view.py,sha256=
|
|
27
|
+
pymmcore_plus/core/events/_device_signal_view.py,sha256=NiM7qImggEsOEgfBJtxlr2mUIx5y184PLYIV_vgIo3w,1328
|
|
27
28
|
pymmcore_plus/core/events/_norm_slot.py,sha256=8DCBoLHGh7cbB1OB19IJYwL6sFBFmkD8IakfBOvFbw8,2907
|
|
28
29
|
pymmcore_plus/core/events/_prop_event_mixin.py,sha256=FvJJnpEKrOR-_Sp3-NNCwFoUUHwmNKiHruo0Y1vybsY,4042
|
|
29
|
-
pymmcore_plus/core/events/_protocol.py,sha256=
|
|
30
|
-
pymmcore_plus/core/events/_psygnal.py,sha256=
|
|
31
|
-
pymmcore_plus/core/events/_qsignals.py,sha256=
|
|
30
|
+
pymmcore_plus/core/events/_protocol.py,sha256=0EpuRrgbnG_abE8raixViU4QcJ59i2RWI7kwKIeZiBs,9072
|
|
31
|
+
pymmcore_plus/core/events/_psygnal.py,sha256=PogTZBA2Xd1PaKHtWpqkSqYOwRd4UM2XSHAHo7-UipI,2752
|
|
32
|
+
pymmcore_plus/core/events/_qsignals.py,sha256=aQfLz_E7E3gqE62KWmuyxekQB9N2rUZ9Dbd8c-TgV9U,4052
|
|
32
33
|
pymmcore_plus/experimental/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
34
|
pymmcore_plus/experimental/unicore/__init__.py,sha256=2jjw8K3DZ8NSY5mFn4YBTrYp78Zc9EbGIZCiyG7PujQ,682
|
|
34
|
-
pymmcore_plus/experimental/unicore/_device_manager.py,sha256=
|
|
35
|
+
pymmcore_plus/experimental/unicore/_device_manager.py,sha256=nhL6YtvmghWRLST8QPNfBIA5A1dfgvxOZE-PARI1_PQ,6820
|
|
35
36
|
pymmcore_plus/experimental/unicore/_proxy.py,sha256=QPFnG9Mw6WCFSui8nUCabJTJL-Xyvyo-fbLM1Q949is,4703
|
|
36
37
|
pymmcore_plus/experimental/unicore/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
38
|
pymmcore_plus/experimental/unicore/core/_sequence_buffer.py,sha256=uyTKacCZNgxmsssBoAt62TvZjtv892YaTojyIDicgRI,12050
|
|
38
|
-
pymmcore_plus/experimental/unicore/core/_unicore.py,sha256=
|
|
39
|
+
pymmcore_plus/experimental/unicore/core/_unicore.py,sha256=oTvkpFlgRILMBx_fSlABw_E9jHia1OX_00D4Ss9qoDQ,72544
|
|
39
40
|
pymmcore_plus/experimental/unicore/devices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
41
|
pymmcore_plus/experimental/unicore/devices/_camera.py,sha256=Ef0VATVVNldZoYlQv7Qykgw1I6G-jk2usk7qhjJFXJ8,8538
|
|
41
42
|
pymmcore_plus/experimental/unicore/devices/_device_base.py,sha256=rmxzP-Cghn9bOA0m3I2a-7CspJAsp3OwTY6mv3bYzE0,10870
|
|
@@ -46,11 +47,11 @@ pymmcore_plus/experimental/unicore/devices/_slm.py,sha256=L2axjzcNQlCSADkuYFwJlZ
|
|
|
46
47
|
pymmcore_plus/experimental/unicore/devices/_stage.py,sha256=NO96VqeLTdtEWBQ3fPerdQhvGK6Nmu57ZKSq5g9jMiA,7897
|
|
47
48
|
pymmcore_plus/experimental/unicore/devices/_state.py,sha256=EpZAI9jYEQ9hU-H873ow9xY1u0oEXCs5lq6Op8-e-Xs,6091
|
|
48
49
|
pymmcore_plus/mda/__init__.py,sha256=7VH-MqOcuK1JNSOG9HhES6Ac-Z-LuT8a0f2xPbGEt7w,344
|
|
49
|
-
pymmcore_plus/mda/_engine.py,sha256=
|
|
50
|
+
pymmcore_plus/mda/_engine.py,sha256=0TmfM-y-fo2TlBTuAeaqZgSDFM3vUKqJxk7T_ze0cyE,38799
|
|
50
51
|
pymmcore_plus/mda/_protocol.py,sha256=10CDJ9H57oX1z0oqK3eShXyQhufHvvu3_8wdaCYpPIg,3254
|
|
51
52
|
pymmcore_plus/mda/_runner.py,sha256=NSOhpll6_WxDLO19FTs19dASJcHcOoVOCy7q_QzX_Ms,18523
|
|
52
53
|
pymmcore_plus/mda/_thread_relay.py,sha256=Ww-9gyvLEzwRhnpL1dpze71wL7IRlhH8K3Q1dmJIxgs,6193
|
|
53
|
-
pymmcore_plus/mda/events/__init__.py,sha256=
|
|
54
|
+
pymmcore_plus/mda/events/__init__.py,sha256=Y3AermCe3megT48VMF9-6lGweAx1J13hAC_TZjvtw6M,1160
|
|
54
55
|
pymmcore_plus/mda/events/_protocol.py,sha256=9xs9sRaCGtOQcAPChsB_NxXs4g8eaEKuJHBzNRRtiII,1739
|
|
55
56
|
pymmcore_plus/mda/events/_psygnal.py,sha256=TdN1mFGpTPXmEs9iwFKSC1svv87PDZkT2JZvl0tEGrQ,640
|
|
56
57
|
pymmcore_plus/mda/events/_qsignals.py,sha256=tULQg-e_NX197DxJXaWHn1zLJ-4tzc9QyOAnsobEDtA,554
|
|
@@ -62,6 +63,7 @@ pymmcore_plus/mda/handlers/_ome_zarr_writer.py,sha256=cKg3kJR7TId6M2qC1nJMLlxkv5
|
|
|
62
63
|
pymmcore_plus/mda/handlers/_tensorstore_handler.py,sha256=YrKwahypq1yoXP3cOcsJTGF-f4LOLrp4KuNHYzPBK64,15321
|
|
63
64
|
pymmcore_plus/mda/handlers/_util.py,sha256=pZydpKAXtQ_gjq5x1yNK1D0hfS7NUL2nH9ivOBg4abc,1600
|
|
64
65
|
pymmcore_plus/metadata/__init__.py,sha256=0o_v53kwR4U_RLlCnr7GD1G6OdFlVuUByIqXiaaM5uk,699
|
|
66
|
+
pymmcore_plus/metadata/_ome.py,sha256=tSY2SUKA1PNAexyde2fEX1a1owsbU_rsHx64H2ywR4s,16513
|
|
65
67
|
pymmcore_plus/metadata/functions.py,sha256=Nw2zMbJx0c6aJs6I_uaLGz6cop0IIPfRZOR-qx-SQbc,12937
|
|
66
68
|
pymmcore_plus/metadata/schema.py,sha256=NxKujQChIXFT48OirNebankGaHNAD0GcA77tjkG4uGs,18390
|
|
67
69
|
pymmcore_plus/metadata/serialize.py,sha256=hpXJm0tzILELf6OYECMg0sQhuf-h25ob6_DDl-TUUME,3805
|
|
@@ -74,8 +76,8 @@ pymmcore_plus/model/_device.py,sha256=AX3rO2gbY7AXJyMN3FfI_n2jl2V0IAPuBh7MiDA5Sq
|
|
|
74
76
|
pymmcore_plus/model/_microscope.py,sha256=69VV6cuevinOK_LhYEkQygHGesvCZefdn9YNt3mV618,11353
|
|
75
77
|
pymmcore_plus/model/_pixel_size_config.py,sha256=RXk8AAwARe8clsXue0GZfOTb1bxyXIsO0ibcDLHM4_s,3889
|
|
76
78
|
pymmcore_plus/model/_property.py,sha256=NQzNtnEzSCR9ogwx1cfi8X-qbJ_cBSJKdSBAaoKoPQ0,3720
|
|
77
|
-
pymmcore_plus-0.
|
|
78
|
-
pymmcore_plus-0.
|
|
79
|
-
pymmcore_plus-0.
|
|
80
|
-
pymmcore_plus-0.
|
|
81
|
-
pymmcore_plus-0.
|
|
79
|
+
pymmcore_plus-0.16.0.dist-info/METADATA,sha256=ob6RhMldyjqQmKL35L2rBZHCftp_npNd1PKLv6m5bQM,8868
|
|
80
|
+
pymmcore_plus-0.16.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
81
|
+
pymmcore_plus-0.16.0.dist-info/entry_points.txt,sha256=NtFyndrQzBpUNJyil-8e5hMGke2utAf7mkGavTLcLOY,51
|
|
82
|
+
pymmcore_plus-0.16.0.dist-info/licenses/LICENSE,sha256=OHJjRpOPKKRc7FEnpehNWdR5LRBdBhUtIFG-ZI0dCEA,1522
|
|
83
|
+
pymmcore_plus-0.16.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|