pymmcore-plus 0.15.4__py3-none-any.whl → 0.17.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/_ipy_completion.py +1 -1
- pymmcore_plus/_logger.py +3 -3
- pymmcore_plus/_util.py +9 -245
- pymmcore_plus/core/_device.py +57 -13
- pymmcore_plus/core/_mmcore_plus.py +20 -23
- pymmcore_plus/core/_property.py +35 -29
- pymmcore_plus/core/_sequencing.py +2 -0
- pymmcore_plus/core/events/_device_signal_view.py +8 -1
- pymmcore_plus/experimental/simulate/__init__.py +88 -0
- pymmcore_plus/experimental/simulate/_objects.py +670 -0
- pymmcore_plus/experimental/simulate/_render.py +510 -0
- pymmcore_plus/experimental/simulate/_sample.py +156 -0
- pymmcore_plus/experimental/unicore/__init__.py +2 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +46 -13
- pymmcore_plus/experimental/unicore/core/_config.py +706 -0
- pymmcore_plus/experimental/unicore/core/_unicore.py +834 -18
- pymmcore_plus/experimental/unicore/devices/_device_base.py +13 -0
- pymmcore_plus/experimental/unicore/devices/_hub.py +50 -0
- pymmcore_plus/experimental/unicore/devices/_stage.py +46 -1
- pymmcore_plus/experimental/unicore/devices/_state.py +6 -0
- pymmcore_plus/install.py +149 -18
- pymmcore_plus/mda/_engine.py +268 -73
- pymmcore_plus/mda/handlers/_5d_writer_base.py +16 -5
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +7 -1
- pymmcore_plus/metadata/_ome.py +553 -0
- pymmcore_plus/metadata/functions.py +2 -1
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/METADATA +7 -4
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/RECORD +35 -27
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/WHEEL +1 -1
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/licenses/LICENSE +0 -0
pymmcore_plus/core/_property.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import weakref
|
|
3
4
|
from functools import cached_property
|
|
4
5
|
from typing import TYPE_CHECKING, Any, TypedDict
|
|
5
6
|
|
|
@@ -58,29 +59,34 @@ class DeviceProperty:
|
|
|
58
59
|
) -> None:
|
|
59
60
|
self.device = device_label
|
|
60
61
|
self.name = property_name
|
|
61
|
-
self.
|
|
62
|
+
self._mmc_ref = weakref.ref(mmcore)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def core(self) -> CMMCorePlus:
|
|
66
|
+
"""Return the `CMMCorePlus` instance to which this Property is bound."""
|
|
67
|
+
if (mmc := self._mmc_ref()) is None: # pragma: no cover
|
|
68
|
+
raise RuntimeError(
|
|
69
|
+
"The CMMCorePlus instance to which this Property "
|
|
70
|
+
"is bound has been deleted."
|
|
71
|
+
)
|
|
72
|
+
return mmc
|
|
62
73
|
|
|
63
74
|
@cached_property
|
|
64
75
|
def valueChanged(self) -> _DevicePropValueSignal:
|
|
65
|
-
return _DevicePropValueSignal(self.device, self.name, self.
|
|
76
|
+
return _DevicePropValueSignal(self.device, self.name, self.core)
|
|
66
77
|
|
|
67
78
|
def isValid(self) -> bool:
|
|
68
79
|
"""Return `True` if device is loaded and has a property by this name."""
|
|
69
|
-
return self.isLoaded() and self.
|
|
80
|
+
return self.isLoaded() and self.core.hasProperty(self.device, self.name)
|
|
70
81
|
|
|
71
82
|
def isLoaded(self) -> bool:
|
|
72
83
|
"""Return true if the device name is loaded."""
|
|
73
|
-
return self.
|
|
74
|
-
|
|
75
|
-
@property
|
|
76
|
-
def core(self) -> CMMCorePlus:
|
|
77
|
-
"""Return the `CMMCorePlus` instance to which this Property is bound."""
|
|
78
|
-
return self._mmc
|
|
84
|
+
return self.core is not None and self.device in self.core.getLoadedDevices()
|
|
79
85
|
|
|
80
86
|
@property
|
|
81
87
|
def value(self) -> Any:
|
|
82
88
|
"""Return current property value, cast to appropriate type if applicable."""
|
|
83
|
-
v = self.
|
|
89
|
+
v = self.core.getProperty(self.device, self.name)
|
|
84
90
|
if type_ := self.type().to_python():
|
|
85
91
|
v = type_(v)
|
|
86
92
|
return v
|
|
@@ -92,7 +98,7 @@ class DeviceProperty:
|
|
|
92
98
|
|
|
93
99
|
def fromCache(self) -> Any:
|
|
94
100
|
"""Return cached property value."""
|
|
95
|
-
return self.
|
|
101
|
+
return self.core.getPropertyFromCache(self.device, self.name)
|
|
96
102
|
|
|
97
103
|
def setValue(self, val: Any) -> None:
|
|
98
104
|
"""Functional alternate to property setter."""
|
|
@@ -103,7 +109,7 @@ class DeviceProperty:
|
|
|
103
109
|
f"'{self.device}::{self.name}' is a read-only property.", stacklevel=2
|
|
104
110
|
)
|
|
105
111
|
try:
|
|
106
|
-
self.
|
|
112
|
+
self.core.setProperty(self.device, self.name, val)
|
|
107
113
|
except RuntimeError as e:
|
|
108
114
|
msg = str(e)
|
|
109
115
|
if allowed := self.allowedValues():
|
|
@@ -112,23 +118,23 @@ class DeviceProperty:
|
|
|
112
118
|
|
|
113
119
|
def isReadOnly(self) -> bool:
|
|
114
120
|
"""Return `True` if property is read only."""
|
|
115
|
-
return self.
|
|
121
|
+
return self.core.isPropertyReadOnly(self.device, self.name)
|
|
116
122
|
|
|
117
123
|
def isPreInit(self) -> bool:
|
|
118
124
|
"""Return `True` if property must be defined prior to initialization."""
|
|
119
|
-
return self.
|
|
125
|
+
return self.core.isPropertyPreInit(self.device, self.name)
|
|
120
126
|
|
|
121
127
|
def hasLimits(self) -> bool:
|
|
122
128
|
"""Return `True` if property has limits."""
|
|
123
|
-
return self.
|
|
129
|
+
return self.core.hasPropertyLimits(self.device, self.name)
|
|
124
130
|
|
|
125
131
|
def lowerLimit(self) -> float:
|
|
126
132
|
"""Return lower limit if property has limits, or 0 otherwise."""
|
|
127
|
-
return self.
|
|
133
|
+
return self.core.getPropertyLowerLimit(self.device, self.name)
|
|
128
134
|
|
|
129
135
|
def upperLimit(self) -> float:
|
|
130
136
|
"""Return upper limit if property has limits, or 0 otherwise."""
|
|
131
|
-
return self.
|
|
137
|
+
return self.core.getPropertyUpperLimit(self.device, self.name)
|
|
132
138
|
|
|
133
139
|
def range(self) -> tuple[float, float]:
|
|
134
140
|
"""Return (lowerLimit, upperLimit) range tuple."""
|
|
@@ -136,31 +142,31 @@ class DeviceProperty:
|
|
|
136
142
|
|
|
137
143
|
def type(self) -> PropertyType:
|
|
138
144
|
"""Return `PropertyType` of this property."""
|
|
139
|
-
return self.
|
|
145
|
+
return self.core.getPropertyType(self.device, self.name)
|
|
140
146
|
|
|
141
147
|
def deviceType(self) -> DeviceType:
|
|
142
148
|
"""Return `DeviceType` of the device owning this property."""
|
|
143
|
-
return self.
|
|
149
|
+
return self.core.getDeviceType(self.device)
|
|
144
150
|
|
|
145
151
|
def allowedValues(self) -> tuple[str, ...]:
|
|
146
|
-
"""Return allowed values for this property, if
|
|
152
|
+
"""Return allowed values for this property, if constrained."""
|
|
147
153
|
# https://github.com/micro-manager/mmCoreAndDevices/issues/172
|
|
148
|
-
allowed = self.
|
|
154
|
+
allowed = self.core.getAllowedPropertyValues(self.device, self.name)
|
|
149
155
|
if not allowed and self.deviceType() is DeviceType.StateDevice:
|
|
150
156
|
if self.name == Keyword.State:
|
|
151
|
-
n_states = self.
|
|
157
|
+
n_states = self.core.getNumberOfStates(self.device)
|
|
152
158
|
allowed = tuple(str(i) for i in range(n_states))
|
|
153
159
|
elif self.name == Keyword.Label:
|
|
154
|
-
allowed = self.
|
|
160
|
+
allowed = self.core.getStateLabels(self.device)
|
|
155
161
|
return allowed
|
|
156
162
|
|
|
157
163
|
def isSequenceable(self) -> bool:
|
|
158
164
|
"""Return `True` if property can be used in a sequence."""
|
|
159
|
-
return self.
|
|
165
|
+
return self.core.isPropertySequenceable(self.device, self.name)
|
|
160
166
|
|
|
161
167
|
def sequenceMaxLength(self) -> int:
|
|
162
168
|
"""Return maximum number of property events that can be put in a sequence."""
|
|
163
|
-
return self.
|
|
169
|
+
return self.core.getPropertySequenceMaxLength(self.device, self.name)
|
|
164
170
|
|
|
165
171
|
def loadSequence(self, eventSequence: Sequence[str]) -> None:
|
|
166
172
|
"""Transfer a sequence of events/states/whatever to the device.
|
|
@@ -171,15 +177,15 @@ class DeviceProperty:
|
|
|
171
177
|
The sequence of events/states that the device will execute in response
|
|
172
178
|
to external triggers
|
|
173
179
|
"""
|
|
174
|
-
self.
|
|
180
|
+
self.core.loadPropertySequence(self.device, self.name, eventSequence)
|
|
175
181
|
|
|
176
182
|
def startSequence(self) -> None:
|
|
177
183
|
"""Start an ongoing sequence of triggered events in a property."""
|
|
178
|
-
self.
|
|
184
|
+
self.core.startPropertySequence(self.device, self.name)
|
|
179
185
|
|
|
180
186
|
def stopSequence(self) -> None:
|
|
181
187
|
"""Stop an ongoing sequence of triggered events in a property."""
|
|
182
|
-
self.
|
|
188
|
+
self.core.stopPropertySequence(self.device, self.name)
|
|
183
189
|
|
|
184
190
|
def dict(self) -> InfoDict:
|
|
185
191
|
"""Return dict of info about this Property.
|
|
@@ -224,5 +230,5 @@ class DeviceProperty:
|
|
|
224
230
|
|
|
225
231
|
def __repr__(self) -> str:
|
|
226
232
|
v = f"value={self.value!r}" if self.isValid() else "INVALID"
|
|
227
|
-
core = repr(self.
|
|
233
|
+
core = repr(self.core).strip("<>")
|
|
228
234
|
return f"<Property '{self.device}::{self.name}' on {core}: {v}>"
|
|
@@ -348,6 +348,8 @@ class EventCombiner:
|
|
|
348
348
|
z_pos=first_event.z_pos,
|
|
349
349
|
exposure=first_event.exposure,
|
|
350
350
|
channel=first_event.channel,
|
|
351
|
+
min_start_time=first_event.min_start_time,
|
|
352
|
+
reset_event_timer=first_event.reset_event_timer,
|
|
351
353
|
)
|
|
352
354
|
|
|
353
355
|
# -------------- helper methods to query props & max lengths ----------------
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import weakref
|
|
3
4
|
from typing import TYPE_CHECKING, Any
|
|
4
5
|
|
|
5
6
|
if TYPE_CHECKING:
|
|
@@ -14,7 +15,13 @@ class _DevicePropValueSignal:
|
|
|
14
15
|
) -> None:
|
|
15
16
|
self._dev = device_label
|
|
16
17
|
self._prop = property_name
|
|
17
|
-
self.
|
|
18
|
+
self._mmc_ref = weakref.ref(mmcore)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def _mmc(self) -> CMMCorePlus:
|
|
22
|
+
if (mmc := self._mmc_ref()) is None: # pragma: no cover
|
|
23
|
+
raise RuntimeError("CMMCorePlus instance has been garbage collected.")
|
|
24
|
+
return mmc
|
|
18
25
|
|
|
19
26
|
def connect(self, callback: _C) -> _C:
|
|
20
27
|
sig = self._mmc.events.devicePropertyChanged(self._dev, self._prop)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Simulated microscope sample for testing and development.
|
|
2
|
+
|
|
3
|
+
This module provides tools for creating virtual microscope samples that
|
|
4
|
+
integrate with CMMCorePlus. When a sample is installed on a core, image
|
|
5
|
+
acquisition returns rendered images based on the sample objects and
|
|
6
|
+
current microscope state (stage position, exposure, pixel size, etc.).
|
|
7
|
+
|
|
8
|
+
Examples
|
|
9
|
+
--------
|
|
10
|
+
Create a sample with some objects and use it with a core:
|
|
11
|
+
|
|
12
|
+
>>> from pymmcore_plus import CMMCorePlus
|
|
13
|
+
>>> from pymmcore_plus.experimental.simulate import (
|
|
14
|
+
... Sample,
|
|
15
|
+
... Point,
|
|
16
|
+
... Line,
|
|
17
|
+
... Rectangle,
|
|
18
|
+
... RenderConfig,
|
|
19
|
+
... )
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Create core and load config
|
|
22
|
+
>>> core = CMMCorePlus.instance()
|
|
23
|
+
>>> core.loadSystemConfiguration()
|
|
24
|
+
>>>
|
|
25
|
+
>>> # Define sample objects (coordinates in microns)
|
|
26
|
+
>>> sample = Sample(
|
|
27
|
+
... [
|
|
28
|
+
... Point(0, 0, intensity=200, radius=5),
|
|
29
|
+
... Point(50, 50, intensity=150, radius=3),
|
|
30
|
+
... Line((0, 0), (100, 100), intensity=100),
|
|
31
|
+
... Rectangle((20, 20), width=30, height=20, intensity=180, fill=True),
|
|
32
|
+
... ]
|
|
33
|
+
... )
|
|
34
|
+
>>>
|
|
35
|
+
>>> # Use as context manager
|
|
36
|
+
>>> with sample.patch(core):
|
|
37
|
+
... core.snapImage()
|
|
38
|
+
... img = core.getImage() # Returns rendered simulation
|
|
39
|
+
... print(img.shape, img.dtype)
|
|
40
|
+
|
|
41
|
+
Custom render configuration:
|
|
42
|
+
|
|
43
|
+
>>> config = RenderConfig(
|
|
44
|
+
... noise_std=5.0, # More noise
|
|
45
|
+
... defocus_scale=0.2, # More blur with Z
|
|
46
|
+
... shot_noise=False, # Disable shot noise
|
|
47
|
+
... bit_depth=16, # 16-bit output
|
|
48
|
+
... )
|
|
49
|
+
>>> sample = Sample([Point(0, 0)], config=config)
|
|
50
|
+
|
|
51
|
+
Manual install/uninstall:
|
|
52
|
+
|
|
53
|
+
>>> sample.install(core)
|
|
54
|
+
>>> # ... do stuff ...
|
|
55
|
+
>>> sample.uninstall()
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from ._objects import (
|
|
59
|
+
Arc,
|
|
60
|
+
Bitmap,
|
|
61
|
+
Bounds,
|
|
62
|
+
Ellipse,
|
|
63
|
+
Line,
|
|
64
|
+
Point,
|
|
65
|
+
Polygon,
|
|
66
|
+
Rectangle,
|
|
67
|
+
RegularPolygon,
|
|
68
|
+
SampleObject,
|
|
69
|
+
rects_intersect,
|
|
70
|
+
)
|
|
71
|
+
from ._render import RenderConfig
|
|
72
|
+
from ._sample import Sample
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"Arc",
|
|
76
|
+
"Bitmap",
|
|
77
|
+
"Bounds",
|
|
78
|
+
"Ellipse",
|
|
79
|
+
"Line",
|
|
80
|
+
"Point",
|
|
81
|
+
"Polygon",
|
|
82
|
+
"Rectangle",
|
|
83
|
+
"RegularPolygon",
|
|
84
|
+
"RenderConfig",
|
|
85
|
+
"Sample",
|
|
86
|
+
"SampleObject",
|
|
87
|
+
"rects_intersect",
|
|
88
|
+
]
|