21"""Abstract Base Classes for the different device types.
32from ast
import literal_eval
33from enum
import EnumMeta
34from threading
import Thread
42_logger = logging.getLogger(__name__)
52 "bool": (type(
None),),
53 "enum": (list, EnumMeta, dict, tuple),
55 "tuple": (type(
None),),
59def _call_if_callable(f):
60 """Call callables, or return value of non-callables."""
61 return f()
if callable(f)
else f
68 name: the setting's name.
69 dtype: a data type from `"int"`, `"float"`, `"bool"`,
70 `"enum"`, or `"str"` (see `DTYPES`).
71 get_func: a function to get the current value.
72 set_func: a function to set the value.
73 values: a description of allowed values dependent on dtype, or
74 function that returns a description.
75 readonly: an optional function to indicate if the setting is
76 readonly. A setting may be readonly temporarily, so this
77 function will return `True` or `False` to indicate its
78 current state. If set to no `None` (default), then its
79 value will be dependent on the value of `set_func`.
81 A client needs some way of knowing a setting name and data type,
82 retrieving the current value and, if settable, a way to retrieve
83 allowable values, and set the value.
85 Setters and getters accept or return:
87 * the setting value for int, float, bool and str;
88 * the setting index into a list, dict or Enum type for enum.
92 refactor into subclasses to avoid if isinstance .. elif
93 .. else. Settings classes should be private: devices should
94 use a factory method rather than instantiate settings
95 directly; most already use add_setting for this.
103 get_func: typing.Optional[typing.Callable[[], typing.Any]],
104 set_func: typing.Optional[typing.Callable[[typing.Any],
None]] =
None,
105 values: typing.Any =
None,
106 readonly: typing.Optional[typing.Callable[[], bool]] =
None,
109 if dtype
not in DTYPES:
110 raise ValueError(
"Unsupported dtype.")
111 elif not (isinstance(values, DTYPES[dtype])
or callable(values)):
113 "Invalid values type for %s '%s': expected function or %s"
114 % (dtype, name, DTYPES[dtype])
120 if self.
_get is not None:
131 if self.
_set is None:
136 if self.
_set is None:
138 "`readonly` is not `None` but `set_func` is `None`"
152 if self.
_get is not None:
156 if isinstance(self.
_values, EnumMeta):
157 return self.
_values(value).value
161 def readonly(self) -> bool:
164 def set(self, value) -> None:
166 if self.
_set is None:
167 raise NotImplementedError()
169 if isinstance(self.
_values, EnumMeta):
174 if isinstance(self.
_values, EnumMeta):
175 return [(v.value, v.name)
for v
in self.
_values]
176 values = _call_if_callable(self.
_values)
177 if values
is not None:
178 if self.
dtype ==
"enum":
179 if isinstance(values, dict):
180 return list(values.items())
183 return list(enumerate(values))
189 """A mixin for devices that 'float'.
191 Some SDKs handling multiple devices do not allow for explicit
192 selection of a specific device. Instead, when the SDK is
193 initialised it assigns an index to each device. However, this
194 index is only unique until the program ends and next time the
195 program runs the device might be assigned a different index. This
196 means that it is not possible to request a specific device to the
197 SDK. Instead, one connects to one of the available devices, then
198 initialises it, and only then can one check which one we got.
200 Floating devices are a problem in systems where there are multiple
201 devices of the same type but we only want to initialise a subset
202 of them. Make sure that a device really is a floating device
203 before making use of this class. Avoid it if possible.
205 This class is a mixin which enforces the implementation of a
206 `get_id` method, which typically returns the device serial number.
209 index: the index of the device on a shared library. This
210 argument is added by the device_server program.
214 def __init__(self, index: int, **kwargs) ->
None:
215 super().__init__(**kwargs)
220 """Return a unique hardware identifier such as a serial number."""
224class TriggerTargetMixin(metaclass=abc.ABCMeta):
225 """Mixin for a device that may be the target of a hardware trigger.
229 Need some way to retrieve the supported trigger types and
230 modes. This is not just two lists, one for types and another
231 for modes, because some modes can only be used with certain
232 types and vice-versa.
238 def trigger_mode(self) -> microscope.TriggerMode:
239 raise NotImplementedError()
243 def trigger_type(self) -> microscope.TriggerType:
244 raise NotImplementedError()
250 """Set device for a specific trigger."""
251 raise NotImplementedError()
255 """Actual trigger of the device.
257 Classes implementing this interface should implement this
258 method instead of `trigger`.
261 raise NotImplementedError()
266 The actual effect is device type dependent. For example, on a
267 `Camera` it triggers image acquisition while on a
268 `DeformableMirror` it applies a queued pattern. See
269 documentation for the devices implementing this interface for
273 microscope.IncompatibleStateError: if trigger type is not
274 set to `TriggerType.SOFTWARE`.
277 if self.
trigger_type is not microscope.TriggerType.SOFTWARE:
279 "trigger type is not software"
281 _logger.debug(
"trigger by software")
286 """A base device class. All devices should subclass this class."""
288 def __init__(self) -> None:
290 self.
_settings: typing.Dict[str, _Setting] = {}
292 def __del__(self) -> None:
295 def get_is_enabled(self) -> bool:
299 """Do any device-specific work on disable.
301 Subclasses should override this method rather than modify
308 """Disable the device for a short period for inactivity."""
313 """Do any device specific work on enable.
315 Subclasses should override this method, rather than modify
322 """Enable the device."""
325 except Exception
as err:
326 _logger.debug(
"Error in _do_enable:", exc_info=err)
330 """Private method - actual shutdown of the device.
332 Users should be calling :meth:`shutdown` and not this method.
333 Concrete implementations should implement this method instead
337 raise NotImplementedError()
339 def initialize(self) -> None:
340 """Initialize the device.
342 If devices have this method (not required, and many don't),
343 then they should call it as part of the initialisation, i.e.,
344 they should call it on their `__init__` method.
350 """Shutdown the device.
352 Disable and disconnect the device. This method should be
353 called before destructing the device object, to ensure that
354 the device is actually shutdown.
356 After `shutdown`, the device object is no longer usable and
357 calling any other method is undefined behaviour. The only
358 exception `shutdown` itself which can be called consecutively,
359 and after the first time will have no effect.
361 A device object that has been shutdown can't be reinitialised.
362 Instead of reusing the object, a new one should be created
363 instead. This means that `shutdown` will leave the device in
364 a state that it can be reconnected.
366 .. code-block:: python
368 device = SomeDevice()
371 # Multiple calls to shutdown are OK
375 # After shutdown, everything else is undefined behaviour.
376 device.enable() # undefined behaviour
377 device.get_setting("speed") # undefined behaviour
379 # To reinitialise the device, construct a new instance.
380 device = SomeDevice()
385 While `__del__` calls `shutdown`, one should not rely on
386 it. Python does not guarante that `__del__` will be
387 called when the interpreter exits so if `shutdown` is not
388 called explicitely, the devices might not be shutdown.
393 except Exception
as e:
394 _logger.warning(
"Exception in disable() during shutdown: %s", e)
395 _logger.info(
"Shutting down ... ... ...")
397 _logger.info(
"... ... ... ... shut down completed.")
406 readonly: typing.Optional[typing.Callable[[], bool]] =
None,
408 """Add a setting definition.
411 name: the setting's name.
412 dtype: a data type from `"int"`, `"float"`, `"bool"`,
413 `"enum"`, or `"str"` (see `DTYPES`).
414 get_func: a function to get the current value.
415 set_func: a function to set the value.
416 values: a description of allowed values dependent on
417 dtype, or function that returns a description.
418 readonly: an optional function to indicate if the setting
419 is readonly. A setting may be readonly temporarily,
420 so this function will return `True` or `False` to
421 indicate its current state. If set to no `None`
422 (default), then its value will be dependent on the
425 A client needs some way of knowing a setting name and data
426 type, retrieving the current value and, if settable, a way to
427 retrieve allowable values, and set the value. We store this
428 info in a dictionary. I considered having a `Setting1 class
429 with getter, setter, etc., and adding `Setting` instances as
430 device attributes, but Pyro does not support dot notation to
431 access the functions we need (e.g. `Device.some_setting.set`),
432 so I'd have to write access functions, anyway.
435 if dtype
not in DTYPES:
436 raise ValueError(
"Unsupported dtype.")
437 elif not (isinstance(values, DTYPES[dtype])
or callable(values)):
439 "Invalid values type for %s '%s': expected function or %s"
440 % (dtype, name, DTYPES[dtype])
444 name, dtype, get_func, set_func, values, readonly
448 """Return the current value of a setting."""
451 except Exception
as err:
452 _logger.error(
"in get_setting(%s):", name, exc_info=err)
456 """Return ordered settings as a list of dicts."""
462 except Exception
as err:
463 _logger.error(
"getting %s: %s", f.__self__.name, err)
466 return {k: catch(v.get)
for k, v
in self.
_settings.items()}
472 except Exception
as err:
473 _logger.error(
"in set_setting(%s):", name, exc_info=err)
477 """Return ordered setting descriptions as a list of dicts."""
481 """Return ordered setting descriptions as a list of dicts."""
482 return [(k, v.describe())
for (k, v)
in self.
_settings.items()]
485 """Update settings based on dict of settings and values."""
489 their_keys = set(incoming.keys())
490 update_keys = my_keys & their_keys
491 if update_keys != my_keys:
492 missing =
", ".join([k
for k
in my_keys - their_keys])
494 "update_settings init=True but missing keys: %s." % missing
501 their_keys = set(incoming.keys())
504 for key
in my_keys & their_keys
509 for key
in update_keys:
510 if key
not in my_keys
or not self.
_settings[key].set:
512 results[key] = NotImplemented
513 update_keys.remove(key)
519 for key
in update_keys:
524def keep_acquiring(func):
525 """Wrapper to preserve acquiring state of data capture devices."""
527 def wrapper(self, *args, **kwargs):
530 result = func(self, *args, **kwargs)
533 result = func(self, *args, **kwargs)
540 """A data capture device.
542 This class handles a thread to fetch data from a device and dispatch
543 it to a client. The client is set using set_client(uri) or (legacy)
546 Derived classed should implement::
548 * :meth:`abort` (required)
549 * :meth:`_fetch_data` (required)
550 * :meth:`_process_data` (optional)
552 Derived classes may override `__init__`, `enable` and `disable`,
553 but must ensure to call this class's implementations as indicated
558 def __init__(self, buffer_length: int = 0, **kwargs) ->
None:
559 """Derived.__init__ must call this at some point."""
585 set_setting = keep_acquiring(Device.set_setting)
589 """Stop acquisition as soon as possible."""
593 """Enable the data capture device.
595 Ensures that a data handling threads are running. Implement
596 device specific code in `_do_enable`.
599 _logger.debug(
"Enabling ...")
603 except Exception
as err:
604 _logger.debug(
"Error in _do_enable:", exc_info=err)
627 _logger.debug(
"... enabled.")
630 """Disable the data capture device.
632 Implement device-specific code in `_do_disable`.
644 """Poll for data and return it, with minimal processing.
646 If the device uses buffering in software, this function should
647 copy the data from the buffer, release or recycle the buffer,
648 then return a reference to the copy. Otherwise, if the SDK
649 returns a data object that will not be written to again, this
650 function can just return a reference to the object. If no
651 data is available, return `None`.
657 """Do any data processing and return data."""
661 """Dispatch data to the client."""
669 if hasattr(client,
"put"):
672 client.receiveData(data, timestamp)
674 Pyro4.errors.ConnectionClosedError,
675 Pyro4.errors.CommunicationError,
679 "Removing %s from client stack: disconnected.", client._pyroUri
685 """Process data and send results to any client."""
691 if isinstance(data, Exception):
692 standard_exception = Exception(str(data).encode(
"ascii"))
694 self.
_send_data(client, standard_exception, timestamp)
695 except Exception
as e:
702 except Exception
as e:
707 _logger.error(
"in _dispatch_loop:", exc_info=err)
711 """Poll source for data and put it into dispatch buffer."""
717 except Exception
as e:
718 _logger.error(
"in _fetch_loop:", exc_info=e)
721 timestamp = time.time()
722 self.
_put(e, timestamp)
726 timestamp = time.time()
727 self.
_put(data, timestamp)
733 """A getter for the current client."""
737 def _client(self, val):
738 """Push or pop a client from the _clientStack."""
745 def _put(self, data, timestamp) -> None:
746 """Put data and timestamp into dispatch buffer with target dispatch client."""
750 """Set up a connection to our client.
752 Clients now sit in a stack so that a single device may send
753 different data to multiple clients in a single experiment.
754 The usage is currently::
756 device.set_client(client) # Add client to top of stack
757 # do stuff, send triggers, receive data
758 device.set_client(None) # Pop top client off stack.
760 There is a risk that some other client calls ``None`` before
761 the current client is finished. Avoiding this will require
762 rework here to identify the caller and remove only that caller
763 from the client stack.
766 if new_client
is not None:
767 if isinstance(new_client, (str, Pyro4.core.URI)):
775 _logger.info(
"Current client is None.")
781 """Update settings, toggling acquisition if necessary."""
786 """A passthrough for compatibility."""
790 """Returns results from next trigger via a direct call.
793 soft_trigger: calls :meth:`trigger` if `True`, waits for
794 hardware trigger if `False`.
813 """Unblocks grab_next_frame so it can return."""
820 """Adds functionality to :class:`DataDevice` to support cameras.
822 Defines the interface for cameras. Applies a transform to
823 acquired data in the processing step.
827 ALLOWED_TRANSFORMS = [p
for p
in itertools.product(*3 * [[
False,
True]])]
846 lambda: Camera.ALLOWED_TRANSFORMS.index(self.
_transform),
847 lambda index: self.
set_transform(Camera.ALLOWED_TRANSFORMS[index]),
848 Camera.ALLOWED_TRANSFORMS,
860 """Apply self._transform to data."""
866 data = numpy.rot90(data, rot)
870 (0, 1): numpy.flipud,
871 (1, 0): numpy.fliplr,
872 (1, 1):
lambda d: numpy.fliplr(numpy.flipud(d)),
876 def set_readout_mode(self, description):
877 """Set the readout mode and _readout_transform."""
881 """Return the current transform without readout transform."""
882 return self._client_transform
885 """Combine provided transform with readout transform."""
886 if isinstance(transform, str):
887 transform = literal_eval(transform)
898 """Update readout transform and update resultant transform."""
904 """Set the exposure time on the device in seconds."""
908 """Return the current exposure time in seconds."""
912 """Return the cycle time in seconds."""
917 """Return a tuple of `(width, height)` indicating shape in pixels."""
921 """Return a tuple of `(width, height)` corrected for transform."""
922 shape = self._get_sensor_shape()
923 if self._transform[2]:
925 shape = (shape[1], shape[0])
930 """Return the current binning."""
934 """Return the current binning corrected for transform."""
935 binning = self._get_binning()
936 if self._transform[2]:
943 """Set binning along both axes. Return `True` if successful."""
947 """Set binning along both axes. Return `True` if successful."""
948 h_bin, v_bin = binning
958 """Return the ROI as it is on hardware."""
959 raise NotImplementedError()
961 def get_roi(self) -> microscope.ROI:
962 """Return current ROI."""
971 """Set the ROI on the hardware. Return `True` if successful."""
974 def set_roi(self, roi: microscope.ROI) ->
None:
975 """Set the ROI according to the provided rectangle.
977 Return True if ROI set correctly, False otherwise.
981 left, top, width, height = roi
983 width = maxw // binning.h
985 height = maxh // binning.v
994 """Mixin for devices that are controlled via serial.
996 DEPRECATED: turns out that this was a bad idea. A device that has
997 a serial connection is not a serial connection. The "has a" and
998 the not "is a" should have told us that we should have been
999 using composition instead of subclassing, but there you go.
1001 Currently handles the flushing and locking of the comms channel
1002 until a command has finished, and the passthrough to the serial
1007 def __init__(self, **kwargs):
1008 super().__init__(**kwargs)
1017 """Read a line from connection without leading and trailing whitespace."""
1021 """Send a command to the device.
1023 This is not a simple passthrough to ``serial.Serial.write``,
1024 it will append ``b'\\r\\n'`` to command. Override this method
1025 if a device requires a specific format.
1027 return self.
connection.write(command + b
"\r\n")
1031 """Decorator to flush input buffer and lock communications.
1033 There have been problems with the DeepStar lasers returning
1034 junk characters after the expected response, so it is
1035 advisable to flush the input buffer prior to running a command
1036 and subsequent readline. It also locks the comms channel so
1037 that a function must finish all its communications before
1042 @functools.wraps(func)
1043 def wrapper(self, *args, **kwargs):
1046 return func(self, *args, **kwargs)
1052 """Base class for Deformable Mirrors.
1054 There is no method to reset or clear a deformable mirror. While
1055 different vendors provide functions to do that, it is unclear
1056 exactly what it does the actuators. Does it set all actuators
1057 back to something based on a calibration file? Does it apply a
1058 voltage of zero to each? Does it set the values to zero and what
1059 does that mean since different deformable mirrors expect values in
1060 a different range? For the sake of uniformity, it is better for
1061 python-microscope users to pass the pattern they want, probably a
1062 pattern that flattens the mirror.
1064 It is also unclear what the use case for a reset. If it just to
1065 set the mirror to an initial state and not a specific shape, then
1066 destroying and re-constructing the DeformableMirror object
1067 provides the most obvious solution.
1069 The private properties `_patterns` and `_pattern_idx` are
1070 initialized to `None` to support the queueing of patterns and
1071 software triggering.
1076 def __init__(self, **kwargs) -> None:
1077 super().__init__(**kwargs)
1078 self.
_patterns: typing.Optional[numpy.ndarray] =
None
1083 def n_actuators(self) -> int:
1084 raise NotImplementedError()
1087 """Validate the shape of a series of patterns.
1089 Only validates the shape of the patterns, not if the values
1090 are actually in the [0 1] range. If some hardware is unable
1091 to handle values outside their defined range (most will simply
1092 clip them), then it's the responsability of the subclass to do
1093 the clipping before sending the values.
1096 if patterns.ndim > 2:
1098 "PATTERNS has %d dimensions (must be 1 or 2)" % patterns.ndim
1103 "PATTERNS length of second dimension '%d' differs"
1104 " from number of actuators '%d'"
1110 def _do_apply_pattern(self, pattern: numpy.ndarray) ->
None:
1111 raise NotImplementedError()
1114 """Apply this pattern.
1117 microscope.IncompatibleStateError: if device trigger type is
1118 not set to software.
1121 if self.
trigger_type is not microscope.TriggerType.SOFTWARE:
1127 "apply_pattern requires software trigger type"
1133 """Send values to the mirror.
1136 patterns: An `KxN` elements array of values in the range
1137 `[0 1]`, where `N` equals the number of actuators, and
1138 `K` is the number of patterns.
1140 A convenience fallback is provided for software triggering is
1149 """Apply the next pattern in the queue.
1151 DEPRECATED: this is the same as calling :meth:`trigger`.
1157 """Convenience fallback.
1159 This only provides a convenience fallback for devices that
1160 don't support queuing multiple patterns and software trigger,
1161 i.e., devices that take only one pattern at a time. This is
1162 not the case of most devices.
1164 Devices that support queuing patterns, should override this
1169 Instead of a convenience fallback, we should have a
1170 separate mixin for this.
1179 """Apply the next pattern in the queue."""
1186 """Light source such as lasers or LEDs.
1188 Light sources often, possibly always, only support the
1189 `TriggerMode.BULB`. In this context, the trigger type changes
1190 what happens when `enable` is called. `TriggerType.SOFTWARE`
1191 means that `enable` will make the device emit light immediately,
1192 and disable will make the device stop emit light.
1194 `TriggerType.HIGH` or `TriggerType.LOW` means that `enable` will
1195 set and unset the laser such that it only emits light while
1196 receiving a high or low TTL, or digital, input signal.
1201 def __init__(self, **kwargs):
1202 super().__init__(**kwargs)
1207 """Query and return the light source status."""
1213 """Return True if the light source is currently able to produce light."""
1218 """Internal function that actually returns the light source power."""
1219 raise NotImplementedError()
1223 """Internal function that actually sets the light source power.
1225 This function will be called by the `power` attribute setter
1226 after clipping the argument to the [0, 1] interval.
1229 raise NotImplementedError()
1233 """Light source power in the [0, 1] interval."""
1238 """Light source power in the [0, 1] interval.
1240 The power value will be clipped to [0, 1] interval.
1242 clipped_power = max(min(power, 1.0), 0.0)
1247 """Return the power set point."""
1252 """ABC for filter wheels, cube turrets, and filter sliders.
1254 FilterWheel devices are devices that have specific positions to
1255 hold different filters. Implementations will enable the change to
1256 any of those positions, including positions that may not hold a
1260 positions: total number of filter positions on this device.
1264 def __init__(self, positions: int, **kwargs) ->
None:
1265 super().__init__(**kwargs)
1268 "positions must be a positive number (was %d)" % positions
1284 """Number of wheel positions."""
1288 def position(self) -> int:
1289 """Number of wheel positions (zero-based)."""
1293 def position(self, new_position: int) ->
None:
1298 "can't move to position %d, limits are [0 %d]"
1303 def _do_get_position(self) -> int:
1304 raise NotImplementedError()
1307 def _do_set_position(self, position: int) ->
None:
1308 raise NotImplementedError()
1312 """Deprecated, use the `n_positions` property."""
1315 def get_position(self) -> int:
1318 def set_position(self, position: int) ->
None:
1323 """Device that controls multiple devices.
1325 Controller devices usually control multiple stage devices,
1326 typically a XY and Z stage, a filterwheel, and a light source.
1327 Controller devices also include multi light source engines.
1329 Each of the controlled devices requires a name. The choice of
1330 name and its documentation is left to the concrete class.
1332 Shutting down a controller device must shutdown the controlled
1333 devices. Concrete classes should be careful to prevent that the
1334 shutdown of a controlled device does not shutdown the controller
1335 and the other controlled devices. This might require that
1336 controlled devices do nothing as part of their shutdown.
1342 def devices(self) -> typing.Mapping[str, Device]:
1343 """Map of names to the controlled devices."""
1344 raise NotImplementedError()
1347 for d
in self.
devices.values():
1352 """A single dimension axis for a :class:`StageDevice`.
1354 A `StageAxis` represents a single axis of a stage and is not a
1355 :class:`Device` instance on itself. Even stages with a single
1356 axis, such as Z-axis piezos, are implemented as a `StageDevice`
1357 composed of a single `StageAxis` instance.
1359 The interface for `StageAxis` maps to that of `StageDevice` so
1360 refer to its documentation.
1366 """Move axis by given amount."""
1367 raise NotImplementedError()
1371 """Move axis to specified position."""
1372 raise NotImplementedError()
1377 """Current axis position."""
1378 raise NotImplementedError()
1383 """Upper and lower limits values for position."""
1384 raise NotImplementedError()
1388 """A stage device, composed of :class:`StageAxis` instances.
1390 A stage device can have any number of axes and dimensions. For a
1391 single `StageDevice` instance each axis has a name that uniquely
1392 identifies it. The names of the individual axes are hardware
1393 dependent and will be part of the concrete class documentation.
1394 They are typically strings such as `"x"` or `"y"`.
1396 .. code-block:: python
1398 stage = SomeStageDevice()
1399 stage.enable() # may trigger a stage move
1402 stage.move_to({'x': 42.0, 'y': -5.1})
1403 stage.move_by({'x': -5.3, 'y': 14.6})
1405 # Individual StageAxis can be controlled directly.
1406 x_axis = stage.axes['x']
1407 y_axis = stage.axes['y']
1408 x_axis.move_to(42.0)
1409 y_axis.move_by(-5.3)
1411 Not all stage devices support simultaneous move of multiple axes.
1412 Because of this, there is no guarantee that move operations with
1413 multiple axes are done simultaneously. Refer to the concrete
1414 class documentation for hardware specific details.
1416 If a move operation involves multiple axes and there is no support
1417 for simultaneous move, the order of the moves is undefined. If a
1418 specific order is required, one can either call the move functions
1419 multiple times in the expected order, or do so via the individual
1422 .. code-block:: python
1424 # Move the x axis first, then mvoe the y axis:
1425 stage.move_by({'x': 10})
1426 stage.move_by({'y': 4})
1428 # The same thing but via the individual axes:
1429 stage.axes['x'].move_by(10)
1430 stage.axes['y'].move_by(4)
1432 Move operations will not attempt to move a stage beyond its
1433 limits. If a call to the move functions would require the stage
1434 to move beyond its limits the move operation is clipped to the
1435 axes limits. No exception is raised.
1437 .. code-block:: python
1439 # Moves x axis to the its upper limit:
1440 x_axis.move_to(x_axis.limits.upper)
1442 # The same as above since the move operations are clipped to
1443 # the axes limits automatically.
1445 x_axis.move_to(math.inf)
1446 x_axis.move_by(math.inf)
1448 Some stages need to find a reference position, home, before being
1449 able to be moved. If required, this happens automatically during
1450 :func:`enable` (see also :func:`may_move_on_enable`).
1455 def axes(self) -> typing.Mapping[str, StageAxis]:
1456 """Map of axis names to the corresponding :class:`StageAxis`.
1458 .. code-block:: python
1460 for name, axis in stage.axes.items():
1461 print(f'moving axis named {name}')
1464 If an axis is not available then it is not included, i.e.,
1465 given a stage with optional axes the missing axes will *not*
1466 appear on the returned dict with a value of `None` or some
1467 other special `StageAxis` instance.
1469 raise NotImplementedError()
1474 """Whether calling :func:`enable` is likely to make the stage move.
1476 Most stages need to be driven to their limits at startup to
1477 find a repeatable zero position and sometimes to find their
1478 limits as well. This is typically called "homing".
1480 Stages that need to "home" differ on how often they need it
1481 but they only do it during :func:`enable`. They may need to
1482 move each time `enable` is called, the first time after the
1483 `Stage` object has been created, or even only the first time
1484 since the device was powered up.
1486 Note the "*may*" on "may_move_on_enable". This is because it
1487 can be difficult to know for certain if `enable` will cause
1488 the stage to home. Still, knowing that the stage *may* move
1489 is essential for safety. An unexpected movement of the stage,
1490 particularly large movements such as moving to the stage
1491 limits, can destroy a sample on the stage --- or even worse,
1492 it can damage an objective or the stage itself. When in
1493 doubt, implementations should return `True`.
1496 raise NotImplementedError()
1501 """Map of axis name to their current position.
1503 .. code-block:: python
1505 for name, position in stage.position.items():
1506 print(f'{name} axis is at position {position}')
1508 The units of the position is the same as the ones being
1509 currently used for the absolute move (:func:`move_to`)
1512 return {name: axis.position
for name, axis
in self.
axes.items()}
1515 def limits(self) -> typing.Mapping[str, microscope.AxisLimits]:
1516 """Map of axis name to its upper and lower limits.
1518 .. code-block:: python
1520 for name, limits in stage.limits.items():
1521 print(f'{name} axis lower limit is {limits.lower}')
1522 print(f'{name} axis upper limit is {limits.upper}')
1524 These are the limits currently imposed by the device or
1525 underlying software and may change over the time of the
1526 `StageDevice` object.
1528 The units of the limits is the same as the ones being
1529 currently used for the move operations.
1532 return {name: axis.limits
for name, axis
in self.
axes.items()}
1535 def move_by(self, delta: typing.Mapping[str, float]) ->
None:
1536 """Move axes by the corresponding amounts.
1539 delta: map of axis name to the amount to be moved.
1541 .. code-block:: python
1543 # Move 'x' axis by 10.2 units and the y axis by -5 units:
1544 stage.move_by({'x': 10.2, 'y': -5})
1546 # The above is equivalent, but possibly faster than:
1547 stage.axes['x'].move_by(10.2)
1548 stage.axes['y'].move_by(-5)
1550 The axes will not move beyond :func:`limits`. If `delta`
1551 would move an axis beyond it limit, no exception is raised.
1552 Instead, the stage will move until the axis limit.
1558 raise NotImplementedError()
1561 def move_to(self, position: typing.Mapping[str, float]) ->
None:
1562 """Move axes to the corresponding positions.
1565 position: map of axis name to the positions to move to.
1567 .. code-block:: python
1569 # Move 'x' axis to position 8 and the y axis to position -5.3
1570 stage.move_to({'x': 8, 'y': -5.3})
1572 # The above is equivalent to
1573 stage.axes['x'].move_to(8)
1574 stage.axes['y'].move_to(-5.3)
1576 The axes will not move beyond :func:`limits`. If `positions`
1577 is beyond the limits, no exception is raised. Instead, the
1578 stage will move until the axes limit.
1581 raise NotImplementedError()
None set_binning(self, microscope.Binning binning)
float get_exposure_time(self)
float get_cycle_time(self)
microscope.ROI get_roi(self)
_set_binning(self, microscope.Binning binning)
typing.Tuple[int, int] get_sensor_shape(self)
None set_roi(self, microscope.ROI roi)
_set_roi(self, microscope.ROI roi)
microscope.ROI _get_roi(self)
None set_exposure_time(self, float value)
typing.Tuple[int, int] _get_sensor_shape(self)
None __init__(self, **kwargs)
set_readout_mode(self, description)
_process_data(self, data)
microscope.Binning _get_binning(self)
_set_readout_transform(self, new_transform)
set_transform(self, transform)
microscope.Binning get_binning(self)
typing.Mapping[str, Device] devices(self)
_send_data(self, client, data, timestamp)
None update_settings(self, settings, bool init=False)
None _dispatch_loop(self)
None receiveData(self, data, timestamp)
None _put(self, data, timestamp)
None set_client(self, new_client)
_process_data(self, data)
None receiveClient(self, str client_uri)
grab_next_data(self, bool soft_trigger=True)
None __init__(self, int buffer_length=0, **kwargs)
None add_setting(self, name, dtype, get_func, set_func, values, typing.Optional[typing.Callable[[], bool]] readonly=None)
get_setting(self, str name)
update_settings(self, incoming, bool init=False)
None set_setting(self, str name, value)
describe_setting(self, str name)
int get_num_positions(self)
None set_position(self, int position)
int _do_get_position(self)
None _do_set_position(self, int position)
None position(self, int new_position)
float get_set_power(self)
float _do_get_power(self)
None _do_set_power(self, float power)
typing.List[str] get_status(self)
int _write(self, bytes command)
None move_to(self, float pos)
None move_by(self, float delta)
microscope.AxisLimits limits(self)
typing.Mapping[str, microscope.AxisLimits] limits(self)
None move_by(self, typing.Mapping[str, float] delta)
typing.Mapping[str, StageAxis] axes(self)
bool may_move_on_enable(self)
None move_to(self, typing.Mapping[str, float] position)
typing.Mapping[str, float] position(self)
microscope.TriggerType trigger_type(self)
None set_trigger(self, microscope.TriggerType ttype, microscope.TriggerMode tmode)