pymmcore-plus 0.9.4__py3-none-any.whl → 0.13.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 (68) hide show
  1. pymmcore_plus/__init__.py +7 -4
  2. pymmcore_plus/_benchmark.py +203 -0
  3. pymmcore_plus/_build.py +6 -1
  4. pymmcore_plus/_cli.py +131 -31
  5. pymmcore_plus/_logger.py +19 -10
  6. pymmcore_plus/_pymmcore.py +12 -0
  7. pymmcore_plus/_util.py +133 -30
  8. pymmcore_plus/core/__init__.py +5 -0
  9. pymmcore_plus/core/_config.py +6 -4
  10. pymmcore_plus/core/_config_group.py +4 -3
  11. pymmcore_plus/core/_constants.py +135 -10
  12. pymmcore_plus/core/_device.py +4 -4
  13. pymmcore_plus/core/_metadata.py +3 -3
  14. pymmcore_plus/core/_mmcore_plus.py +254 -170
  15. pymmcore_plus/core/_property.py +6 -6
  16. pymmcore_plus/core/_sequencing.py +370 -233
  17. pymmcore_plus/core/events/__init__.py +6 -6
  18. pymmcore_plus/core/events/_device_signal_view.py +8 -6
  19. pymmcore_plus/core/events/_norm_slot.py +2 -4
  20. pymmcore_plus/core/events/_prop_event_mixin.py +7 -4
  21. pymmcore_plus/core/events/_protocol.py +5 -2
  22. pymmcore_plus/core/events/_psygnal.py +2 -2
  23. pymmcore_plus/experimental/__init__.py +0 -0
  24. pymmcore_plus/experimental/unicore/__init__.py +14 -0
  25. pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
  26. pymmcore_plus/experimental/unicore/_proxy.py +127 -0
  27. pymmcore_plus/experimental/unicore/_unicore.py +703 -0
  28. pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  29. pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
  30. pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
  31. pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
  32. pymmcore_plus/install.py +16 -11
  33. pymmcore_plus/mda/__init__.py +1 -1
  34. pymmcore_plus/mda/_engine.py +320 -148
  35. pymmcore_plus/mda/_protocol.py +6 -4
  36. pymmcore_plus/mda/_runner.py +62 -51
  37. pymmcore_plus/mda/_thread_relay.py +5 -3
  38. pymmcore_plus/mda/events/__init__.py +2 -2
  39. pymmcore_plus/mda/events/_protocol.py +10 -2
  40. pymmcore_plus/mda/events/_psygnal.py +2 -2
  41. pymmcore_plus/mda/handlers/_5d_writer_base.py +106 -15
  42. pymmcore_plus/mda/handlers/__init__.py +7 -1
  43. pymmcore_plus/mda/handlers/_img_sequence_writer.py +11 -6
  44. pymmcore_plus/mda/handlers/_ome_tiff_writer.py +8 -4
  45. pymmcore_plus/mda/handlers/_ome_zarr_writer.py +82 -9
  46. pymmcore_plus/mda/handlers/_tensorstore_handler.py +374 -0
  47. pymmcore_plus/mda/handlers/_util.py +1 -1
  48. pymmcore_plus/metadata/__init__.py +36 -0
  49. pymmcore_plus/metadata/functions.py +353 -0
  50. pymmcore_plus/metadata/schema.py +472 -0
  51. pymmcore_plus/metadata/serialize.py +120 -0
  52. pymmcore_plus/mocks.py +51 -0
  53. pymmcore_plus/model/_config_file.py +5 -6
  54. pymmcore_plus/model/_config_group.py +29 -2
  55. pymmcore_plus/model/_core_device.py +12 -1
  56. pymmcore_plus/model/_core_link.py +2 -1
  57. pymmcore_plus/model/_device.py +39 -8
  58. pymmcore_plus/model/_microscope.py +39 -3
  59. pymmcore_plus/model/_pixel_size_config.py +27 -4
  60. pymmcore_plus/model/_property.py +13 -3
  61. pymmcore_plus/seq_tester.py +1 -1
  62. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -11
  63. pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
  64. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
  65. pymmcore_plus/core/_state.py +0 -244
  66. pymmcore_plus-0.9.4.dist-info/RECORD +0 -55
  67. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
  68. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -10,27 +10,18 @@ from collections import defaultdict
10
10
  from contextlib import contextmanager, suppress
11
11
  from datetime import datetime
12
12
  from pathlib import Path
13
+ from re import Pattern
13
14
  from textwrap import dedent
14
- from threading import RLock, Thread
15
- from typing import (
16
- TYPE_CHECKING,
17
- Any,
18
- Callable,
19
- Iterable,
20
- Iterator,
21
- NamedTuple,
22
- Pattern,
23
- Sequence,
24
- TypeVar,
25
- overload,
26
- )
15
+ from threading import Thread
16
+ from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, overload
27
17
 
28
- import pymmcore
29
18
  from psygnal import SignalInstance
30
19
 
20
+ import pymmcore_plus._pymmcore as pymmcore
31
21
  from pymmcore_plus._logger import current_logfile, logger
32
22
  from pymmcore_plus._util import find_micromanager, print_tabular_data
33
23
  from pymmcore_plus.mda import MDAEngine, MDARunner, PMDAEngine
24
+ from pymmcore_plus.metadata.functions import summary_metadata
34
25
 
35
26
  from ._adapter import DeviceAdapter
36
27
  from ._config import Configuration
@@ -39,28 +30,26 @@ from ._constants import (
39
30
  DeviceDetectionStatus,
40
31
  DeviceInitializationState,
41
32
  DeviceType,
33
+ FocusDirection,
42
34
  PixelType,
43
35
  PropertyType,
44
36
  )
45
37
  from ._device import Device
46
38
  from ._metadata import Metadata
47
39
  from ._property import DeviceProperty
48
- from ._sequencing import can_sequence_events
49
- from ._state import core_state
50
40
  from .events import CMMCoreSignaler, PCoreSignaler, _get_auto_core_callback_class
51
41
 
52
42
  if TYPE_CHECKING:
53
- from typing import Literal, TypedDict
43
+ from collections.abc import Iterable, Iterator, Sequence
44
+ from typing import Literal, TypedDict, Unpack
54
45
 
55
46
  import numpy as np
56
47
  from useq import MDAEvent
57
48
 
58
49
  from pymmcore_plus.mda._runner import SingleOutput
59
-
60
- from ._state import StateDict
50
+ from pymmcore_plus.metadata.schema import SummaryMetaV1
61
51
 
62
52
  _T = TypeVar("_T")
63
- _F = TypeVar("_F", bound=Callable[..., Any])
64
53
  ListOrTuple = list[_T] | tuple[_T, ...]
65
54
 
66
55
  class PropertySchema(TypedDict, total=False):
@@ -84,10 +73,43 @@ if TYPE_CHECKING:
84
73
  type: str
85
74
  properties: dict[str, PropertySchema]
86
75
 
87
- def synchronized(lock: RLock) -> Callable[[_F], _F]: ...
76
+ class SetContextKwargs(TypedDict, total=False):
77
+ """All the valid keywords and their types for the `setContext` method."""
78
+
79
+ autoFocusDevice: str
80
+ autoFocusOffset: float
81
+ autoShutter: bool
82
+ cameraDevice: str
83
+ channelGroup: str
84
+ circularBufferMemoryFootprint: int
85
+ deviceAdapterSearchPaths: list[str]
86
+ deviceDelayMs: tuple[str, float]
87
+ exposure: float | tuple[str, float]
88
+ focusDevice: str
89
+ focusDirection: str
90
+ galvoDevice: str
91
+ galvoPosition: tuple[str, float, float]
92
+ imageProcessorDevice: str
93
+ multiROI: tuple[list[int], list[int], list[int], list[int]]
94
+ parentLabel: tuple[str, str]
95
+ pixelSizeAffine: tuple[str, list[float]]
96
+ pixelSizeUm: tuple[str, float]
97
+ position: float | tuple[str, float]
98
+ primaryLogFile: str | tuple[str, bool]
99
+ property: tuple[str, str, bool | float | int | str]
100
+ ROI: tuple[int, int, int, int] | tuple[str, int, int, int, int]
101
+ SLMDevice: str
102
+ SLMExposure: tuple[str, float]
103
+ shutterDevice: str
104
+ shutterOpen: bool | tuple[str, bool]
105
+ state: tuple[str, int]
106
+ stateLabel: tuple[str, str]
107
+ systemState: pymmcore.Configuration
108
+ timeoutMs: int
109
+ XYPosition: tuple[float, float] | tuple[str, float, float]
110
+ XYStageDevice: str
111
+ ZPosition: float | tuple[str, float]
88
112
 
89
- else:
90
- from wrapt import synchronized
91
113
 
92
114
  _OBJDEV_REGEX = re.compile("(.+)?(nosepiece|obj(ective)?)(turret)?s?", re.IGNORECASE)
93
115
  _CHANNEL_REGEX = re.compile("(chan{1,2}(el)?|filt(er)?)s?", re.IGNORECASE)
@@ -153,8 +175,6 @@ class CMMCorePlus(pymmcore.CMMCore):
153
175
  Paths to search for device adapters, by default ()
154
176
  """
155
177
 
156
- _lock = RLock()
157
-
158
178
  @classmethod
159
179
  def instance(cls) -> CMMCorePlus:
160
180
  """Return the global singleton instance of `CMMCorePlus`.
@@ -193,6 +213,9 @@ class CMMCorePlus(pymmcore.CMMCore):
193
213
  if _instance is None:
194
214
  _instance = self
195
215
 
216
+ if hasattr("self", "enableFeature"):
217
+ self.enableFeature("StrictInitializationChecks", True)
218
+
196
219
  # TODO: test this on windows ... writing to the same file may be an issue there
197
220
  if logfile := current_logfile(logger):
198
221
  self.setPrimaryLogFile(str(logfile))
@@ -239,10 +262,11 @@ class CMMCorePlus(pymmcore.CMMCore):
239
262
  if hasattr(self, "_weak_clean"):
240
263
  atexit.unregister(self._weak_clean)
241
264
  self.unloadAllDevices()
265
+ # clean up logging
266
+ self.setPrimaryLogFile("")
242
267
 
243
268
  # Re-implemented methods from the CMMCore API
244
269
 
245
- @synchronized(_lock)
246
270
  def setProperty(
247
271
  self, label: str, propName: str, propValue: bool | float | int | str
248
272
  ) -> None:
@@ -257,7 +281,6 @@ class CMMCorePlus(pymmcore.CMMCore):
257
281
  with self._property_change_emission_ensured(label, (propName,)):
258
282
  super().setProperty(label, propName, propValue)
259
283
 
260
- @synchronized(_lock)
261
284
  def setState(self, stateDeviceLabel: str, state: int) -> None:
262
285
  """Set state (by position) on `stateDeviceLabel`, with reliable event emission.
263
286
 
@@ -270,7 +293,6 @@ class CMMCorePlus(pymmcore.CMMCore):
270
293
  with self._property_change_emission_ensured(stateDeviceLabel, STATE_PROPS):
271
294
  super().setState(stateDeviceLabel, state)
272
295
 
273
- @synchronized(_lock)
274
296
  def setStateLabel(self, stateDeviceLabel: str, stateLabel: str) -> None:
275
297
  """Set state (by label) on `stateDeviceLabel`, with reliable event emission.
276
298
 
@@ -327,33 +349,37 @@ class CMMCorePlus(pymmcore.CMMCore):
327
349
  try:
328
350
  super().loadDevice(label, moduleName, deviceName)
329
351
  except RuntimeError as e:
330
- msg = str(e)
331
- if label in self.getLoadedDevices():
332
- lib = super().getDeviceLibrary(label)
333
- name = super().getDeviceName(label)
334
- if moduleName == lib and deviceName == name:
335
- msg += f". Device {label!r} appears to be loaded already."
336
- warnings.warn(msg, stacklevel=2)
337
- return
338
-
339
- msg += f". Device {label!r} is already taken by {lib}::{name}"
352
+ if exc := self._load_error_with_info(label, moduleName, deviceName, str(e)):
353
+ raise exc from e
354
+
355
+ def _load_error_with_info(
356
+ self, label: str, moduleName: str, deviceName: str, msg: str = ""
357
+ ) -> RuntimeError | None:
358
+ if label in self.getLoadedDevices():
359
+ lib = super().getDeviceLibrary(label)
360
+ name = super().getDeviceName(label)
361
+ if moduleName == lib and deviceName == name:
362
+ msg += f". Device {label!r} appears to be loaded already."
363
+ warnings.warn(msg, stacklevel=2)
364
+ return None
365
+
366
+ msg += f". Device {label!r} is already taken by {lib}::{name}"
367
+ else:
368
+ adapters = super().getDeviceAdapterNames()
369
+ if moduleName not in adapters:
370
+ msg += (
371
+ f". Adapter name {moduleName!r} not in list of known adapter "
372
+ f"names: {adapters}."
373
+ )
340
374
  else:
341
- adapters = super().getDeviceAdapterNames()
342
- if moduleName not in adapters:
375
+ devices = super().getAvailableDevices(moduleName)
376
+ if deviceName not in devices:
343
377
  msg += (
344
- f". Adapter name {moduleName!r} not in list of known adapter "
345
- f"names: {adapters}."
378
+ f". Device name {deviceName!r} not in devices provided by "
379
+ f"adapter {moduleName!r}: {devices}"
346
380
  )
347
- else:
348
- devices = super().getAvailableDevices(moduleName)
349
- if deviceName not in devices:
350
- msg += (
351
- f". Device name {deviceName!r} not in devices provided by "
352
- f"adapter {moduleName!r}: {devices}"
353
- )
354
- raise RuntimeError(msg) from e
355
-
356
- @synchronized(_lock)
381
+ return RuntimeError(msg)
382
+
357
383
  def loadSystemConfiguration(
358
384
  self, fileName: str | Path = "MMConfig_demo.cfg"
359
385
  ) -> None:
@@ -400,6 +426,14 @@ class CMMCorePlus(pymmcore.CMMCore):
400
426
  """
401
427
  return DeviceType(super().getDeviceType(label))
402
428
 
429
+ def getFocusDirection(self, stageLabel: str) -> FocusDirection:
430
+ """Return device type for a given device.
431
+
432
+ **Why Override?** The returned [`pymmcore_plus.FocusDirection`][] enum is more
433
+ interpretable than the raw `int` returned by `pymmcore`
434
+ """
435
+ return FocusDirection(super().getFocusDirection(stageLabel))
436
+
403
437
  def getPropertyType(self, label: str, propName: str) -> PropertyType:
404
438
  """Return the intrinsic property type for a given device and property.
405
439
 
@@ -573,7 +607,6 @@ class CMMCorePlus(pymmcore.CMMCore):
573
607
  @overload
574
608
  def getLastImageAndMD(self, *, fix: bool = True) -> tuple[np.ndarray, Metadata]: ...
575
609
 
576
- @synchronized(_lock)
577
610
  def getLastImageAndMD(
578
611
  self, channel: int | None = None, slice: int | None = None, *, fix: bool = True
579
612
  ) -> tuple[np.ndarray, Metadata]:
@@ -609,7 +642,10 @@ class CMMCorePlus(pymmcore.CMMCore):
609
642
  img = super().getLastImageMD(channel, slice, md)
610
643
  else:
611
644
  img = super().getLastImageMD(md)
612
- return (self.fixImage(img) if fix else img, md)
645
+ return (
646
+ self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img,
647
+ md,
648
+ )
613
649
 
614
650
  @overload
615
651
  def popNextImageAndMD(
@@ -619,9 +655,8 @@ class CMMCorePlus(pymmcore.CMMCore):
619
655
  @overload
620
656
  def popNextImageAndMD(self, *, fix: bool = True) -> tuple[np.ndarray, Metadata]: ...
621
657
 
622
- @synchronized(_lock)
623
658
  def popNextImageAndMD(
624
- self, channel: int | None = None, slice: int | None = None, *, fix: bool = True
659
+ self, channel: int = 0, slice: int = 0, *, fix: bool = True
625
660
  ) -> tuple[np.ndarray, Metadata]:
626
661
  """Gets and removes the next image (and metadata) from the circular buffer.
627
662
 
@@ -651,13 +686,13 @@ class CMMCorePlus(pymmcore.CMMCore):
651
686
  Image and metadata
652
687
  """
653
688
  md = Metadata()
654
- if channel is not None and slice is not None:
655
- img = super().popNextImageMD(channel, slice, md)
656
- else:
657
- img = super().popNextImageMD(md)
658
- return (self.fixImage(img) if fix else img, md)
689
+ img = super().popNextImageMD(channel, slice, md)
690
+ md = Metadata(md)
691
+ return (
692
+ self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img,
693
+ md,
694
+ )
659
695
 
660
- @synchronized(_lock)
661
696
  def popNextImage(self, *, fix: bool = True) -> np.ndarray:
662
697
  """Gets and removes the next image from the circular buffer.
663
698
 
@@ -672,9 +707,8 @@ class CMMCorePlus(pymmcore.CMMCore):
672
707
  will be reshaped to (w, h, n_components) using `fixImage`.
673
708
  """
674
709
  img: np.ndarray = super().popNextImage()
675
- return self.fixImage(img) if fix else img
710
+ return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img
676
711
 
677
- @synchronized(_lock)
678
712
  def getNBeforeLastImageAndMD(
679
713
  self, n: int, *, fix: bool = True
680
714
  ) -> tuple[np.ndarray, Metadata]:
@@ -701,7 +735,7 @@ class CMMCorePlus(pymmcore.CMMCore):
701
735
  """
702
736
  md = Metadata()
703
737
  img = super().getNBeforeLastImageMD(n, md)
704
- return self.fixImage(img) if fix else img, md
738
+ return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img, md
705
739
 
706
740
  def setConfig(self, groupName: str, configName: str) -> None:
707
741
  """Applies a configuration to a group.
@@ -1338,35 +1372,6 @@ class CMMCorePlus(pymmcore.CMMCore):
1338
1372
  """
1339
1373
  return self.setPosition(val)
1340
1374
 
1341
- @overload
1342
- def setPosition(self, position: float) -> None: ...
1343
-
1344
- @overload
1345
- def setPosition(self, stageLabel: str, position: float) -> None: ...
1346
-
1347
- @synchronized(_lock)
1348
- def setPosition(self, *args: Any, **kwargs: Any) -> None:
1349
- """Set position of the stage in microns.
1350
-
1351
- **Why Override?** To add a lock to prevent concurrent calls across threads.
1352
- """
1353
- return super().setPosition(*args, **kwargs)
1354
-
1355
- @overload
1356
- def setXYPosition(self, x: float, y: float) -> None: ...
1357
-
1358
- @overload
1359
- def setXYPosition(self, xyStageLabel: str, x: float, y: float) -> None: ...
1360
-
1361
- @synchronized(_lock)
1362
- def setXYPosition(self, *args: Any, **kwargs: Any) -> None:
1363
- """Sets the position of the XY stage in microns.
1364
-
1365
- **Why Override?** To add a lock to prevent concurrent calls across threads.
1366
- """
1367
- return super().setXYPosition(*args, **kwargs)
1368
-
1369
- @synchronized(_lock)
1370
1375
  def getCameraChannelNames(self) -> tuple[str, ...]:
1371
1376
  """Convenience method to call `getCameraChannelName` for all camera channels.
1372
1377
 
@@ -1377,11 +1382,11 @@ class CMMCorePlus(pymmcore.CMMCore):
1377
1382
  for i in range(self.getNumberOfCameraChannels())
1378
1383
  )
1379
1384
 
1380
- @synchronized(_lock)
1381
1385
  def snapImage(self) -> None:
1382
1386
  """Acquires a single image with current settings.
1383
1387
 
1384
- **Why Override?** To add a lock to prevent concurrent calls across threads.
1388
+ **Why Override?** to emit the `imageSnapped` event after snapping an image.
1389
+ and to emit shutter property changes if `getAutoShutter` is `True`.
1385
1390
  """
1386
1391
  if autoshutter := self.getAutoShutter():
1387
1392
  self.events.propertyChanged.emit(self.getShutterDevice(), "State", True)
@@ -1470,7 +1475,11 @@ class CMMCorePlus(pymmcore.CMMCore):
1470
1475
  old_engine = self.mda.set_engine(engine)
1471
1476
  self.events.mdaEngineRegistered.emit(engine, old_engine)
1472
1477
 
1473
- def fixImage(self, img: np.ndarray, ncomponents: int | None = None) -> np.ndarray:
1478
+ def fixImage(
1479
+ self,
1480
+ img: np.ndarray,
1481
+ ncomponents: int | None = None,
1482
+ ) -> np.ndarray:
1474
1483
  """Fix img shape/dtype based on `self.getNumberOfComponents()`.
1475
1484
 
1476
1485
  :sparkles: *This method is new in `CMMCorePlus`.*
@@ -1492,12 +1501,34 @@ class CMMCorePlus(pymmcore.CMMCore):
1492
1501
  """
1493
1502
  if ncomponents is None:
1494
1503
  ncomponents = self.getNumberOfComponents()
1495
- if ncomponents == 4:
1504
+ if ncomponents == 4 and img.ndim != 3:
1496
1505
  new_shape = (*img.shape, 4)
1497
- img = img.view(dtype=f"u{img.dtype.itemsize//4}").reshape(new_shape)
1506
+ img = img.view(dtype=f"u{img.dtype.itemsize // 4}").reshape(new_shape)
1498
1507
  img = img[..., [2, 1, 0]] # Convert from BGRA to RGB
1499
1508
  return img
1500
1509
 
1510
+ def getPhysicalCameraDevice(self, channel_index: int = 0) -> str:
1511
+ """Return the name of the actual camera device for a given channel index.
1512
+
1513
+ :sparkles: *This method is new in `CMMCorePlus`.* It provides a convenience
1514
+ for accessing the name of the actual camera device when using the multi-camera
1515
+ utility.
1516
+ """
1517
+ cam_dev = self.getCameraDevice()
1518
+ # best as I can tell, this is a hard-coded string in Utilities/MultiCamera.cpp
1519
+ # (it also appears in ArduinoCounter.cpp). This appears to be "the way"
1520
+ # to get at the original camera when using the multi-camera utility.
1521
+ prop_name = f"Physical Camera {channel_index + 1}"
1522
+ if self.hasProperty(cam_dev, prop_name):
1523
+ return self.getProperty(cam_dev, prop_name)
1524
+ if channel_index > 0:
1525
+ warnings.warn(
1526
+ f"Camera {cam_dev} does not have a property {prop_name}. "
1527
+ f"Cannot get channel_index={channel_index}",
1528
+ stacklevel=2,
1529
+ )
1530
+ return cam_dev
1531
+
1501
1532
  def getTaggedImage(self, channel_index: int = 0) -> TaggedImage:
1502
1533
  """Return getImage as named tuple with metadata.
1503
1534
 
@@ -1560,7 +1591,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1560
1591
 
1561
1592
  try:
1562
1593
  channel_group = self.getPropertyFromCache("Core", "ChannelGroup")
1563
- channel = self.getCurrentConfigFromCache(channel_group)
1594
+ channel: str = self.getCurrentConfigFromCache(channel_group)
1564
1595
  except Exception:
1565
1596
  channel = "Default"
1566
1597
  tags["Channel"] = channel
@@ -1570,15 +1601,9 @@ class CMMCorePlus(pymmcore.CMMCore):
1570
1601
  tags["Binning"] = self.getProperty(self.getCameraDevice(), "Binning")
1571
1602
 
1572
1603
  if channel_index is not None:
1573
- if "CameraChannelIndex" not in tags:
1574
- tags["CameraChannelIndex"] = channel_index
1575
- tags["ChannelIndex"] = channel_index
1576
- if "Camera" not in tags:
1577
- core_cam = tags.get("Core-Camera")
1578
- phys_cam_key = f"{core_cam}-Physical Camera {channel_index+1}"
1579
- if phys_cam_key in tags:
1580
- tags["Camera"] = tags[phys_cam_key]
1581
- # tags["Channel"] = tags[phys_cam_key] # ?? why did MMCoreJ do this?
1604
+ tags["CameraChannelIndex"] = channel_index
1605
+ tags["ChannelIndex"] = channel_index
1606
+ tags["Camera"] = self.getPhysicalCameraDevice(channel_index)
1582
1607
 
1583
1608
  # these are added by AcqEngJ
1584
1609
  # yyyy-MM-dd HH:mm:ss.mmmmmm # NOTE AcqEngJ omits microseconds
@@ -1643,7 +1668,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1643
1668
  if numChannel is not None
1644
1669
  else super().getImage()
1645
1670
  )
1646
- return self.fixImage(img) if fix else img
1671
+ return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img
1647
1672
 
1648
1673
  def startContinuousSequenceAcquisition(self, intervalMs: float = 0) -> None:
1649
1674
  """Start a ContinuousSequenceAcquisition.
@@ -1887,30 +1912,33 @@ class CMMCorePlus(pymmcore.CMMCore):
1887
1912
  super().definePixelSizeConfig(*args, **kwargs)
1888
1913
  self.events.pixelSizeChanged.emit(0.0)
1889
1914
 
1890
- def getMultiROI( # type: ignore [override]
1891
- self, *_: Any
1892
- ) -> tuple[list[int], list[int], list[int], list[int]]:
1893
- """Get multiple ROIs from the current camera device.
1915
+ # pymmcore-SWIG needs this, but pymmcore-nano doesn't
1916
+ if hasattr(pymmcore, "UnsignedVector"):
1894
1917
 
1895
- Will fail if the camera does not support multiple ROIs. Will return empty
1896
- vectors if multiple ROIs are not currently being used.
1918
+ def getMultiROI( # type: ignore [override]
1919
+ self, *_: Any
1920
+ ) -> tuple[list[int], list[int], list[int], list[int]]:
1921
+ """Get multiple ROIs from the current camera device.
1897
1922
 
1898
- **Why Override?** So that the user doesn't need to pass in four empty
1899
- pymmcore.UnsignedVector() objects.
1900
- """
1901
- if _:
1902
- warnings.warn( # pragma: no cover
1903
- "Unlike pymmcore, CMMCorePlus.getMultiROI does not require arguments."
1904
- "Arguments are ignored.",
1905
- stacklevel=2,
1906
- )
1923
+ Will fail if the camera does not support multiple ROIs. Will return empty
1924
+ vectors if multiple ROIs are not currently being used.
1925
+
1926
+ **Why Override?** So that the user doesn't need to pass in four empty
1927
+ pymmcore.UnsignedVector() objects.
1928
+ """
1929
+ if _:
1930
+ warnings.warn( # pragma: no cover
1931
+ "Unlike pymmcore, CMMCorePlus.getMultiROI does not require "
1932
+ "arguments. Arguments are ignored.",
1933
+ stacklevel=2,
1934
+ )
1907
1935
 
1908
- xs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1909
- ys = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1910
- ws = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1911
- hs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1912
- super().getMultiROI(xs, ys, ws, hs)
1913
- return list(xs), list(ys), list(ws), list(hs)
1936
+ xs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1937
+ ys = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1938
+ ws = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1939
+ hs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1940
+ super().getMultiROI(xs, ys, ws, hs)
1941
+ return list(xs), list(ys), list(ws), list(hs)
1914
1942
 
1915
1943
  @overload
1916
1944
  def setROI(self, x: int, y: int, width: int, height: int) -> None: ...
@@ -1974,7 +2002,12 @@ class CMMCorePlus(pymmcore.CMMCore):
1974
2002
  with open(filename, "a") as f:
1975
2003
  f.write("\n".join(cfg))
1976
2004
 
1977
- def describe(self, sort: str | None = None) -> None:
2005
+ def describe(
2006
+ self,
2007
+ sort: str | None = None,
2008
+ show_config_groups: bool = False,
2009
+ show_available: bool = False,
2010
+ ) -> None:
1978
2011
  """Print information table with the current configuration.
1979
2012
 
1980
2013
  Intended to provide a quick overview of the microscope configuration during
@@ -1982,7 +2015,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1982
2015
 
1983
2016
  :sparkles: *This method is new in `CMMCorePlus`.*
1984
2017
  """
1985
- _current = {
2018
+ _current: dict[str, str] = {
1986
2019
  self.getCameraDevice(): "Camera",
1987
2020
  self.getXYStageDevice(): "XYStage",
1988
2021
  self.getFocusDevice(): "Focus",
@@ -2006,38 +2039,62 @@ class CMMCorePlus(pymmcore.CMMCore):
2006
2039
 
2007
2040
  print(f"{self.getVersionInfo()}, {self.getAPIVersionInfo()}")
2008
2041
  print("Adapter path:", ",".join(self.getDeviceAdapterSearchPaths()))
2042
+ print("\nLoaded Devices:")
2009
2043
  print_tabular_data(data, sort=sort)
2010
2044
 
2045
+ state = self.state(cached=False)
2046
+ if show_config_groups:
2047
+ group_data: defaultdict[str, list[str]] = defaultdict(list)
2048
+ groups = state["config_groups"]
2049
+ for group in groups:
2050
+ for pi, preset in enumerate(group["presets"]):
2051
+ for si, stng in enumerate(preset["settings"]):
2052
+ dev, prop, val = stng["dev"], stng["prop"], stng["val"]
2053
+ group_name = group["name"] if (pi == 0 and si == 0) else ""
2054
+ preset_name = preset["name"] if si == 0 else ""
2055
+ group_data["Group"].append(group_name)
2056
+ group_data["Preset"].append(preset_name)
2057
+ group_data["Device"].append(dev)
2058
+ group_data["Property"].append(prop)
2059
+ group_data["Value"].append(val)
2060
+ # add break between presets
2061
+ group_data["Group"].append("")
2062
+ group_data["Preset"].append("")
2063
+ group_data["Device"].append("")
2064
+ group_data["Property"].append("")
2065
+ group_data["Value"].append("")
2066
+
2067
+ print("\nConfig Groups:")
2068
+ print_tabular_data(group_data, sort=sort)
2069
+
2070
+ if show_available:
2071
+ avail_data: defaultdict[str, list[str]] = defaultdict(list)
2072
+ avail_adapters = self.getDeviceAdapterNames()
2073
+ for adapt in avail_adapters:
2074
+ with suppress(Exception):
2075
+ devices = self.getAvailableDevices(adapt)
2076
+ descriptions = self.getAvailableDeviceDescriptions(adapt)
2077
+ types = self.getAvailableDeviceTypes(adapt)
2078
+ for dev, desc, type_ in zip(devices, descriptions, types):
2079
+ avail_data["Library, DeviceName"].append(f"{adapt!r}, {dev!r}")
2080
+ avail_data["Type"].append(str(DeviceType(type_)))
2081
+ avail_data["Description"].append(desc)
2082
+
2083
+ print("\nAvailable Devices:")
2084
+ print_tabular_data(avail_data, sort=sort)
2085
+
2011
2086
  def state(
2012
- self,
2013
- *,
2014
- devices: bool = True,
2015
- image: bool = True,
2016
- system_info: bool = False,
2017
- system_status: bool = False,
2018
- config_groups: bool | Sequence[str] = True,
2019
- position: bool = False,
2020
- autofocus: bool = False,
2021
- pixel_size_configs: bool = False,
2022
- device_types: bool = False,
2023
- cached: bool = True,
2024
- error_value: Any = None,
2025
- ) -> StateDict:
2087
+ self, *, cached: bool = True, include_time: bool = False, **_kwargs: Any
2088
+ ) -> SummaryMetaV1:
2026
2089
  """Return info on the current state of the core."""
2027
- return core_state(
2028
- self,
2029
- devices=devices,
2030
- image=image,
2031
- system_info=system_info,
2032
- system_status=system_status,
2033
- config_groups=config_groups,
2034
- position=position,
2035
- autofocus=autofocus,
2036
- pixel_size_configs=pixel_size_configs,
2037
- device_types=device_types,
2038
- cached=cached,
2039
- error_value=error_value,
2040
- )
2090
+ if _kwargs:
2091
+ keys = ", ".join(_kwargs.keys())
2092
+ warnings.warn(
2093
+ f"CMMCorePlus.state no longer takes arguments: {keys}. Ignoring."
2094
+ "Please update your code as this may be an error in the future.",
2095
+ stacklevel=2,
2096
+ )
2097
+ return summary_metadata(self, include_time=include_time, cached=cached)
2041
2098
 
2042
2099
  @contextmanager
2043
2100
  def _property_change_emission_ensured(
@@ -2061,8 +2118,18 @@ class CMMCorePlus(pymmcore.CMMCore):
2061
2118
  and self.getDeviceType(device) is DeviceType.StateDevice
2062
2119
  ):
2063
2120
  properties = STATE_PROPS
2121
+ try:
2122
+ before = [self.getProperty(device, p) for p in properties]
2123
+ except Exception as e:
2124
+ logger.error(
2125
+ "Error getting properties %s on %s: %s. Cannot ensure signal emission",
2126
+ properties,
2127
+ device,
2128
+ e,
2129
+ )
2130
+ yield
2131
+ return
2064
2132
 
2065
- before = [self.getProperty(device, p) for p in properties]
2066
2133
  with _blockSignal(self.events, self.events.propertyChanged):
2067
2134
  yield
2068
2135
  after = [self.getProperty(device, p) for p in properties]
@@ -2071,7 +2138,7 @@ class CMMCorePlus(pymmcore.CMMCore):
2071
2138
  self.events.propertyChanged.emit(device, properties[i], val)
2072
2139
 
2073
2140
  @contextmanager
2074
- def setContext(self, **kwargs: Any) -> Iterator[None]:
2141
+ def setContext(self, **kwargs: Unpack[SetContextKwargs]) -> Iterator[None]:
2075
2142
  """Set core properties in a context restoring the initial values on exit.
2076
2143
 
2077
2144
  :sparkles: *This method is new in `CMMCorePlus`.*
@@ -2080,9 +2147,12 @@ class CMMCorePlus(pymmcore.CMMCore):
2080
2147
  ----------
2081
2148
  **kwargs : Any
2082
2149
  Keyword arguments may be any `Name` for which `get<Name>` and `set<Name>`
2083
- methods exist. For example, `setContext(exposure=10)` will call
2150
+ methods exist (where the first letter in `<Name>` may be either lower or
2151
+ upper case). For example, `setContext(exposure=10)` will call
2084
2152
  `setExposure(10)` when entering the context and `setExposure(<initial>)`
2085
- when exiting the context.
2153
+ when exiting the context. If the property is not found, a warning is logged
2154
+ and the property is skipped. If the value is a tuple, it is unpacked and
2155
+ passed to the `set<Name>` method (but lists are not unpacked).
2086
2156
 
2087
2157
  Examples
2088
2158
  --------
@@ -2102,11 +2172,16 @@ class CMMCorePlus(pymmcore.CMMCore):
2102
2172
  try:
2103
2173
  for name, v in kwargs.items():
2104
2174
  name = name[0].upper() + name[1:]
2105
- try:
2106
- orig_values[name] = getattr(self, f"get{name}")()
2107
- getattr(self, f"set{name}")(v)
2108
- except AttributeError:
2175
+ get_name, set_name = f"get{name}", f"set{name}"
2176
+ if not hasattr(self, get_name) or not hasattr(self, set_name):
2109
2177
  logger.warning("%s is not a valid property, skipping.", name)
2178
+ continue
2179
+
2180
+ orig_values[name] = getattr(self, get_name)()
2181
+ if isinstance(v, tuple):
2182
+ getattr(self, set_name)(*v)
2183
+ else:
2184
+ getattr(self, set_name)(v)
2110
2185
  yield
2111
2186
  finally:
2112
2187
  for k, v in orig_values.items():
@@ -2168,7 +2243,16 @@ class CMMCorePlus(pymmcore.CMMCore):
2168
2243
  False
2169
2244
  ```
2170
2245
  """
2171
- return can_sequence_events(self, e1, e2, cur_length)
2246
+ warnings.warn(
2247
+ "canSequenceEvents is deprecated.\nPlease use "
2248
+ "`list(pymmcore_plus.core.iter_sequenced_events(core, [e1, e2]))` "
2249
+ "to see how this core will combine MDAEvents into SequencedEvents.",
2250
+ DeprecationWarning,
2251
+ stacklevel=2,
2252
+ )
2253
+ from ._sequencing import can_sequence_events
2254
+
2255
+ return can_sequence_events(self, e1, e2)
2172
2256
 
2173
2257
 
2174
2258
  for name in (