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()
254 def _do_trigger(self) -> None:
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
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:
298 def _do_disable(self):
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."""
312 def _do_enable(self):
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)
329 def _do_shutdown(self) -> None:
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()
377 device.get_setting(
"speed")
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."""
450 return self._settings[name].get()
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()}
471 self._settings[name].set(value)
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."""
478 return self._settings[name].describe()
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."""
488 my_keys = set(self._settings.keys())
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
500 my_keys = set(self._settings.keys())
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)
515 if self._settings[key].readonly():
517 self._settings[key].set(incoming[key])
519 for key
in update_keys:
520 results[key] = self._settings[key].get()
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`.
643 def _fetch_data(self) -> None:
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`.
656 def _process_data(self, data):
657 """Do any data processing and return data."""
660 def _send_data(self, client, data, timestamp):
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
684 def _dispatch_loop(self) -> None:
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)
710 def _fetch_loop(self) -> None:
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)
758 device.set_client(
None)
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,
859 def _process_data(self, data):
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)),
874 return super()._process_data(data)
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)
897 def _set_readout_transform(self, new_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."""
916 def _get_sensor_shape(self) -> typing.Tuple[int, int]:
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])
929 def _get_binning(self) -> microscope.Binning:
930 """Return the current binning."""
934 """Return the current binning corrected for transform."""
935 binning = self._get_binning()
936 if self._transform[2]:
942 def _set_binning(self, binning: microscope.Binning):
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
957 def _get_roi(self) -> microscope.ROI:
958 """Return the ROI as it is on hardware."""
959 raise NotImplementedError()
962 """Return current ROI."""
970 def _set_roi(self, roi: microscope.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)
1016 def _readline(self) -> bytes:
1017 """Read a line from connection without leading and trailing whitespace."""
1020 def _write(self, command: bytes) -> int:
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()
1086 def _validate_patterns(self, patterns: numpy.ndarray) ->
None:
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.
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`.
1156 def _do_trigger(self) -> None:
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."""
1217 def _do_get_power(self) -> float:
1218 """Internal function that actually returns the light source power."""
1219 raise NotImplementedError()
1222 def _do_set_power(self, power: float) ->
None:
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()
1346 def _do_shutdown(self) -> None:
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()
1402 stage.move_to({
'x': 42.0,
'y': -5.1})
1403 stage.move_by({
'x': -5.3,
'y': 14.6})
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
1425 stage.move_by({
'x': 10})
1426 stage.move_by({
'y': 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
1440 x_axis.move_to(x_axis.limits.upper)
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
1544 stage.move_by({
'x': 10.2,
'y': -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
1570 stage.move_to({
'x': 8,
'y': -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)
def _set_roi(self, microscope.ROI roi)
typing.Tuple[int, int] get_sensor_shape(self)
None set_roi(self, microscope.ROI roi)
microscope.ROI _get_roi(self)
None set_exposure_time(self, float value)
def set_transform(self, transform)
None __init__(self, **kwargs)
def _set_binning(self, microscope.Binning binning)
def set_readout_mode(self, description)
microscope.Binning get_binning(self)
typing.Mapping[str, Device] devices(self)
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)
def _send_data(self, client, data, timestamp)
def _process_data(self, data)
None receiveClient(self, str client_uri)
def 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)
def get_all_settings(self)
def get_setting(self, str name)
def describe_settings(self)
def update_settings(self, incoming, bool init=False)
None set_setting(self, str name, value)
def 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)
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)