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.
Files changed (35) hide show
  1. pymmcore_plus/__init__.py +20 -1
  2. pymmcore_plus/_accumulator.py +23 -5
  3. pymmcore_plus/_cli.py +44 -26
  4. pymmcore_plus/_discovery.py +344 -0
  5. pymmcore_plus/_ipy_completion.py +1 -1
  6. pymmcore_plus/_logger.py +3 -3
  7. pymmcore_plus/_util.py +9 -245
  8. pymmcore_plus/core/_device.py +57 -13
  9. pymmcore_plus/core/_mmcore_plus.py +20 -23
  10. pymmcore_plus/core/_property.py +35 -29
  11. pymmcore_plus/core/_sequencing.py +2 -0
  12. pymmcore_plus/core/events/_device_signal_view.py +8 -1
  13. pymmcore_plus/experimental/simulate/__init__.py +88 -0
  14. pymmcore_plus/experimental/simulate/_objects.py +670 -0
  15. pymmcore_plus/experimental/simulate/_render.py +510 -0
  16. pymmcore_plus/experimental/simulate/_sample.py +156 -0
  17. pymmcore_plus/experimental/unicore/__init__.py +2 -0
  18. pymmcore_plus/experimental/unicore/_device_manager.py +46 -13
  19. pymmcore_plus/experimental/unicore/core/_config.py +706 -0
  20. pymmcore_plus/experimental/unicore/core/_unicore.py +834 -18
  21. pymmcore_plus/experimental/unicore/devices/_device_base.py +13 -0
  22. pymmcore_plus/experimental/unicore/devices/_hub.py +50 -0
  23. pymmcore_plus/experimental/unicore/devices/_stage.py +46 -1
  24. pymmcore_plus/experimental/unicore/devices/_state.py +6 -0
  25. pymmcore_plus/install.py +149 -18
  26. pymmcore_plus/mda/_engine.py +268 -73
  27. pymmcore_plus/mda/handlers/_5d_writer_base.py +16 -5
  28. pymmcore_plus/mda/handlers/_tensorstore_handler.py +7 -1
  29. pymmcore_plus/metadata/_ome.py +553 -0
  30. pymmcore_plus/metadata/functions.py +2 -1
  31. {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/METADATA +7 -4
  32. {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/RECORD +35 -27
  33. {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/WHEEL +1 -1
  34. {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/entry_points.txt +0 -0
  35. {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -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._mmc = mmcore
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._mmc)
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._mmc.hasProperty(self.device, self.name)
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._mmc is not None and self.device in self._mmc.getLoadedDevices()
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._mmc.getProperty(self.device, self.name)
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._mmc.getPropertyFromCache(self.device, self.name)
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._mmc.setProperty(self.device, self.name, val)
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._mmc.isPropertyReadOnly(self.device, self.name)
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._mmc.isPropertyPreInit(self.device, self.name)
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._mmc.hasPropertyLimits(self.device, self.name)
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._mmc.getPropertyLowerLimit(self.device, self.name)
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._mmc.getPropertyUpperLimit(self.device, self.name)
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._mmc.getPropertyType(self.device, self.name)
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._mmc.getDeviceType(self.device)
149
+ return self.core.getDeviceType(self.device)
144
150
 
145
151
  def allowedValues(self) -> tuple[str, ...]:
146
- """Return allowed values for this property, if contstrained."""
152
+ """Return allowed values for this property, if constrained."""
147
153
  # https://github.com/micro-manager/mmCoreAndDevices/issues/172
148
- allowed = self._mmc.getAllowedPropertyValues(self.device, self.name)
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._mmc.getNumberOfStates(self.device)
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._mmc.getStateLabels(self.device)
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._mmc.isPropertySequenceable(self.device, self.name)
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._mmc.getPropertySequenceMaxLength(self.device, self.name)
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._mmc.loadPropertySequence(self.device, self.name, eventSequence)
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._mmc.startPropertySequence(self.device, self.name)
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._mmc.stopPropertySequence(self.device, self.name)
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._mmc).strip("<>")
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._mmc = mmcore
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
+ ]