BioImager  3.9.1
A .NET microscopy imaging library. Supports various microscopes by using imported libraries & GUI automation. Supported libraries include PriorĀ® & ZeissĀ® & all devices supported by Micromanager 2.0 and python-microscope.
Loading...
Searching...
No Matches
abc.py
1#!/usr/bin/env python3
2
3## Copyright (C) 2020 David Miguel Susano Pinto <carandraug@gmail.com>
4## Copyright (C) 2020 Mick Phillips <mick.phillips@gmail.com>
5##
6## This file is part of Microscope.
7##
8## Microscope is free software: you can redistribute it and/or modify
9## it under the terms of the GNU General Public License as published by
10## the Free Software Foundation, either version 3 of the License, or
11## (at your option) any later version.
12##
13## Microscope is distributed in the hope that it will be useful,
14## but WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16## GNU General Public License for more details.
17##
18## You should have received a copy of the GNU General Public License
19## along with Microscope. If not, see <http://www.gnu.org/licenses/>.
20
21"""Abstract Base Classes for the different device types.
22"""
23
24import abc
25import functools
26import itertools
27import logging
28import queue
29import threading
30import time
31import typing
32from ast import literal_eval
33from enum import EnumMeta
34from threading import Thread
35
36import numpy
37import Pyro4
38
39import microscope
40
41
42_logger = logging.getLogger(__name__)
43
44
45# Mapping of setting data types descriptors to allowed-value types.
46#
47# We use a descriptor for the type instead of the actual type because
48# there may not be a unique proper type as for example in enum.
49DTYPES = {
50 "int": (tuple,),
51 "float": (tuple,),
52 "bool": (type(None),),
53 "enum": (list, EnumMeta, dict, tuple),
54 "str": (int,),
55 "tuple": (type(None),),
56}
57
58
59def _call_if_callable(f):
60 """Call callables, or return value of non-callables."""
61 return f() if callable(f) else f
62
63
65 """Create a setting.
66
67 Args:
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`.
80
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.
84
85 Setters and getters accept or return:
86
87 * the setting value for int, float, bool and str;
88 * the setting index into a list, dict or Enum type for enum.
89
90 .. todo::
91
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.
96
97 """
98
99 def __init__(
100 self,
101 name: str,
102 dtype: str,
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,
107 ) -> None:
108 self.name = name
109 if dtype not in DTYPES:
110 raise ValueError("Unsupported dtype.")
111 elif not (isinstance(values, DTYPES[dtype]) or callable(values)):
112 raise TypeError(
113 "Invalid values type for %s '%s': expected function or %s"
114 % (dtype, name, DTYPES[dtype])
115 )
116 self.dtype = dtype
117 self._get = get_func
118 self._values = values
119 self._last_written = None
120 if self._get is not None:
121 self._set = set_func
122 else:
123 # Cache last written value for write-only settings.
124 def w(value):
125 self._last_written = value
126 set_func(value)
127
128 self._set = w
129
130 if readonly is None:
131 if self._set is None:
132 self._readonly = lambda: True
133 else:
134 self._readonly = lambda: False
135 else:
136 if self._set is None:
137 raise ValueError(
138 "`readonly` is not `None` but `set_func` is `None`"
139 )
140 else:
141 self._readonly = readonly
142
143 def describe(self):
144 return {
145 "type": self.dtype,
146 "values": self.values(),
147 "readonly": self.readonly(),
148 "cached": self._last_written is not None,
149 }
150
151 def get(self):
152 if self._get is not None:
153 value = self._get()
154 else:
155 value = self._last_written
156 if isinstance(self._values, EnumMeta):
157 return self._values(value).value
158 else:
159 return value
160
161 def readonly(self) -> bool:
162 return self._readonly()
163
164 def set(self, value) -> None:
165 """Set a setting."""
166 if self._set is None:
167 raise NotImplementedError()
168 # TODO further validation.
169 if isinstance(self._values, EnumMeta):
170 value = self._values(value)
171 self._set(value)
172
173 def values(self):
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())
181 else:
182 # self._values is a list or tuple
183 return list(enumerate(values))
184 elif self._values is not None:
185 return values
186
187
188class FloatingDeviceMixin(metaclass=abc.ABCMeta):
189 """A mixin for devices that 'float'.
190
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.
199
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.
204
205 This class is a mixin which enforces the implementation of a
206 `get_id` method, which typically returns the device serial number.
207
208 Args:
209 index: the index of the device on a shared library. This
210 argument is added by the device_server program.
211
212 """
213
214 def __init__(self, index: int, **kwargs) -> None:
215 super().__init__(**kwargs)
216 self._index = index
217
218 @abc.abstractmethod
219 def get_id(self) -> str:
220 """Return a unique hardware identifier such as a serial number."""
221 pass
222
223
224class TriggerTargetMixin(metaclass=abc.ABCMeta):
225 """Mixin for a device that may be the target of a hardware trigger.
226
227 .. todo::
228
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.
233
234 """
235
236 @property
237 @abc.abstractmethod
238 def trigger_mode(self) -> microscope.TriggerMode:
239 raise NotImplementedError()
240
241 @property
242 @abc.abstractmethod
243 def trigger_type(self) -> microscope.TriggerType:
244 raise NotImplementedError()
245
246 @abc.abstractmethod
248 self, ttype: microscope.TriggerType, tmode: microscope.TriggerMode
249 ) -> None:
250 """Set device for a specific trigger."""
251 raise NotImplementedError()
252
253 @abc.abstractmethod
254 def _do_trigger(self) -> None:
255 """Actual trigger of the device.
256
257 Classes implementing this interface should implement this
258 method instead of `trigger`.
259
260 """
261 raise NotImplementedError()
262
263 def trigger(self) -> None:
264 """Trigger device.
265
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
270 details.
271
272 Raises:
273 microscope.IncompatibleStateError: if trigger type is not
274 set to `TriggerType.SOFTWARE`.
275
276 """
277 if self.trigger_type is not microscope.TriggerType.SOFTWARE:
279 "trigger type is not software"
280 )
281 _logger.debug("trigger by software")
282 self._do_trigger()
283
284
285class Device(metaclass=abc.ABCMeta):
286 """A base device class. All devices should subclass this class."""
287
288 def __init__(self) -> None:
289 self.enabled = False
290 self._settings: typing.Dict[str, _Setting] = {}
291
292 def __del__(self) -> None:
293 self.shutdown()
294
295 def get_is_enabled(self) -> bool:
296 return self.enabled
297
298 def _do_disable(self):
299 """Do any device-specific work on disable.
300
301 Subclasses should override this method rather than modify
302 `disable`.
303
304 """
305 return True
306
307 def disable(self) -> None:
308 """Disable the device for a short period for inactivity."""
309 self._do_disable()
310 self.enabled = False
311
312 def _do_enable(self):
313 """Do any device specific work on enable.
314
315 Subclasses should override this method, rather than modify
316 `enable`.
317
318 """
319 return True
320
321 def enable(self) -> None:
322 """Enable the device."""
323 try:
324 self.enabled = self._do_enable()
325 except Exception as err:
326 _logger.debug("Error in _do_enable:", exc_info=err)
327
328 @abc.abstractmethod
329 def _do_shutdown(self) -> None:
330 """Private method - actual shutdown of the device.
331
332 Users should be calling :meth:`shutdown` and not this method.
333 Concrete implementations should implement this method instead
334 of `shutdown`.
335
336 """
337 raise NotImplementedError()
338
339 def initialize(self) -> None:
340 """Initialize the device.
341
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.
345
346 """
347 pass
348
349 def shutdown(self) -> None:
350 """Shutdown the device.
351
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.
355
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.
360
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.
365
366 .. code-block:: python
367
368 device = SomeDevice()
369 device.shutdown()
370
371 # Multiple calls to shutdown are OK
372 device.shutdown()
373 device.shutdown()
374
375 # After shutdown, everything else is undefined behaviour.
376 device.enable() # undefined behaviour
377 device.get_setting("speed") # undefined behaviour
378
379 # To reinitialise the device, construct a new instance.
380 device = SomeDevice()
381
382
383 .. note::
384
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.
389
390 """
391 try:
392 self.disable()
393 except Exception as e:
394 _logger.warning("Exception in disable() during shutdown: %s", e)
395 _logger.info("Shutting down ... ... ...")
396 self._do_shutdown()
397 _logger.info("... ... ... ... shut down completed.")
398
400 self,
401 name,
402 dtype,
403 get_func,
404 set_func,
405 values,
406 readonly: typing.Optional[typing.Callable[[], bool]] = None,
407 ) -> None:
408 """Add a setting definition.
409
410 Args:
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
423 value of `set_func`.
424
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.
433
434 """
435 if dtype not in DTYPES:
436 raise ValueError("Unsupported dtype.")
437 elif not (isinstance(values, DTYPES[dtype]) or callable(values)):
438 raise TypeError(
439 "Invalid values type for %s '%s': expected function or %s"
440 % (dtype, name, DTYPES[dtype])
441 )
442 else:
443 self._settings[name] = _Setting(
444 name, dtype, get_func, set_func, values, readonly
445 )
446
447 def get_setting(self, name: str):
448 """Return the current value of a setting."""
449 try:
450 return self._settings[name].get()
451 except Exception as err:
452 _logger.error("in get_setting(%s):", name, exc_info=err)
453 raise
454
456 """Return ordered settings as a list of dicts."""
457 # Fetching some settings may fail depending on device state.
458 # Report these values as 'None' and continue fetching other settings.
459 def catch(f):
460 try:
461 return f()
462 except Exception as err:
463 _logger.error("getting %s: %s", f.__self__.name, err)
464 return None
465
466 return {k: catch(v.get) for k, v in self._settings.items()}
467
468 def set_setting(self, name: str, value) -> None:
469 """Set a setting."""
470 try:
471 self._settings[name].set(value)
472 except Exception as err:
473 _logger.error("in set_setting(%s):", name, exc_info=err)
474 raise
475
476 def describe_setting(self, name: str):
477 """Return ordered setting descriptions as a list of dicts."""
478 return self._settings[name].describe()
479
481 """Return ordered setting descriptions as a list of dicts."""
482 return [(k, v.describe()) for (k, v) in self._settings.items()]
483
484 def update_settings(self, incoming, init: bool = False):
485 """Update settings based on dict of settings and values."""
486 if init:
487 # Assume nothing about state: set everything.
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])
493 msg = (
494 "update_settings init=True but missing keys: %s." % missing
495 )
496 _logger.debug(msg)
497 raise Exception(msg)
498 else:
499 # Only update changed values.
500 my_keys = set(self._settings.keys())
501 their_keys = set(incoming.keys())
502 update_keys = set(
503 key
504 for key in my_keys & their_keys
505 if self.get_setting(key) != incoming[key]
506 )
507 results = {}
508 # Update values.
509 for key in update_keys:
510 if key not in my_keys or not self._settings[key].set:
511 # Setting not recognised or no set function implemented
512 results[key] = NotImplemented
513 update_keys.remove(key)
514 continue
515 if self._settings[key].readonly():
516 continue
517 self._settings[key].set(incoming[key])
518 # Read back values in second loop.
519 for key in update_keys:
520 results[key] = self._settings[key].get()
521 return results
522
523
524def keep_acquiring(func):
525 """Wrapper to preserve acquiring state of data capture devices."""
526
527 def wrapper(self, *args, **kwargs):
528 if self._acquiring:
529 self.abort()
530 result = func(self, *args, **kwargs)
531 self._do_enable()
532 else:
533 result = func(self, *args, **kwargs)
534 return result
535
536 return wrapper
537
538
539class DataDevice(Device, metaclass=abc.ABCMeta):
540 """A data capture device.
541
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)
544 receiveClient(uri).
545
546 Derived classed should implement::
547
548 * :meth:`abort` (required)
549 * :meth:`_fetch_data` (required)
550 * :meth:`_process_data` (optional)
551
552 Derived classes may override `__init__`, `enable` and `disable`,
553 but must ensure to call this class's implementations as indicated
554 in the docstrings.
555
556 """
557
558 def __init__(self, buffer_length: int = 0, **kwargs) -> None:
559 """Derived.__init__ must call this at some point."""
560 super().__init__(**kwargs)
561 # A thread to fetch and dispatch data.
562 self._fetch_thread = None
563 # A flag to control the _fetch_thread.
564 self._fetch_thread_run = False
565 # A flag to indicate that this class uses a fetch callback.
566 self._using_callback = False
567 # Clients to which we send data.
568 self._clientStack = []
569 # A set of live clients to avoid repeated dispatch to disconnected client.
570 self._liveClients = set()
571 # A thread to dispatch data.
572 self._dispatch_thread = None
573 # A buffer for data dispatch.
574 self._dispatch_buffer = queue.Queue(maxsize=buffer_length)
575 # A flag to indicate if device is ready to acquire.
576 self._acquiring = False
577 # A condition to signal arrival of a new data and unblock grab_next_data
578 self._new_data_condition = threading.Condition()
579
580 def __del__(self):
581 self.disabledisable()
582 super().__del__()
583
584 # Wrap set_setting to pause and resume acquisition.
585 set_setting = keep_acquiring(Device.set_setting)
586
587 @abc.abstractmethod
588 def abort(self) -> None:
589 """Stop acquisition as soon as possible."""
590 self._acquiring = False
591
592 def enable(self) -> None:
593 """Enable the data capture device.
594
595 Ensures that a data handling threads are running. Implement
596 device specific code in `_do_enable`.
597
598 """
599 _logger.debug("Enabling ...")
600 # Call device-specific code.
601 try:
602 result = self._do_enable()
603 except Exception as err:
604 _logger.debug("Error in _do_enable:", exc_info=err)
605 self.enabledenabled = False
606 raise err
607 if not result:
608 self.enabledenabled = False
609 else:
610 self.enabledenabled = True
611 # Set up data fetching
612 if self._using_callback:
613 if self._fetch_thread:
614 self._fetch_thread_run = False
615 else:
616 if not self._fetch_thread or not self._fetch_thread.is_alive():
617 self._fetch_thread = Thread(target=self._fetch_loop)
618 self._fetch_thread.daemon = True
619 self._fetch_thread.start()
620 if (
621 not self._dispatch_thread
622 or not self._dispatch_thread.is_alive()
623 ):
624 self._dispatch_thread = Thread(target=self._dispatch_loop)
625 self._dispatch_thread.daemon = True
626 self._dispatch_thread.start()
627 _logger.debug("... enabled.")
628
629 def disable(self) -> None:
630 """Disable the data capture device.
631
632 Implement device-specific code in `_do_disable`.
633
634 """
635 self.enabledenabled = False
636 if self._fetch_thread:
637 if self._fetch_thread.is_alive():
638 self._fetch_thread_run = False
639 self._fetch_thread.join()
640 super().disable()
641
642 @abc.abstractmethod
643 def _fetch_data(self) -> None:
644 """Poll for data and return it, with minimal processing.
645
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`.
652
653 """
654 return None
655
656 def _process_data(self, data):
657 """Do any data processing and return data."""
658 return data
659
660 def _send_data(self, client, data, timestamp):
661 """Dispatch data to the client."""
662 try:
663 # Cockpit will send a client with receiveData and expects
664 # two arguments (data and timestamp). But we really want
665 # to use Python's Queue and instead of just the timestamp
666 # we should be sending some proper metadata object. We
667 # don't have that proper metadata class yet so just send
668 # the image data as a numpy ndarray for now.
669 if hasattr(client, "put"):
670 client.put(data)
671 else:
672 client.receiveData(data, timestamp)
673 except (
674 Pyro4.errors.ConnectionClosedError,
675 Pyro4.errors.CommunicationError,
676 ):
677 # Client not listening
678 _logger.info(
679 "Removing %s from client stack: disconnected.", client._pyroUri
680 )
681 self._clientStack = list(filter(client.__ne__, self._clientStack))
682 self._liveClients = self._liveClients.difference([client])
683
684 def _dispatch_loop(self) -> None:
685 """Process data and send results to any client."""
686 while True:
687 client, data, timestamp = self._dispatch_buffer.get(block=True)
688 if client not in self._liveClients:
689 continue
690 err = None
691 if isinstance(data, Exception):
692 standard_exception = Exception(str(data).encode("ascii"))
693 try:
694 self._send_data(client, standard_exception, timestamp)
695 except Exception as e:
696 err = e
697 else:
698 try:
699 self._send_data(
700 client, self._process_data(data), timestamp
701 )
702 except Exception as e:
703 err = e
704 if err:
705 # Raising an exception will kill the dispatch loop. We need
706 # another way to notify the client that there was a problem.
707 _logger.error("in _dispatch_loop:", exc_info=err)
708 self._dispatch_buffer.task_done()
709
710 def _fetch_loop(self) -> None:
711 """Poll source for data and put it into dispatch buffer."""
712 self._fetch_thread_run = True
713
714 while self._fetch_thread_run:
715 try:
716 data = self._fetch_data()
717 except Exception as e:
718 _logger.error("in _fetch_loop:", exc_info=e)
719 # Raising an exception will kill the fetch loop. We need
720 # another way to notify the client that there was a problem.
721 timestamp = time.time()
722 self._put(e, timestamp)
723 data = None
724 if data is not None:
725 # TODO Add support for timestamp from hardware.
726 timestamp = time.time()
727 self._put(data, timestamp)
728 else:
729 time.sleep(0.001)
730
731 @property
732 def _client(self):
733 """A getter for the current client."""
734 return (self._clientStack or [None])[-1]
735
736 @_client.setter
737 def _client(self, val):
738 """Push or pop a client from the _clientStack."""
739 if val is None:
740 self._clientStack.pop()
741 else:
742 self._clientStack.append(val)
743 self._liveClients = set(self._clientStack)
744
745 def _put(self, data, timestamp) -> None:
746 """Put data and timestamp into dispatch buffer with target dispatch client."""
747 self._dispatch_buffer.put((self._client_client_client, data, timestamp))
748
749 def set_client(self, new_client) -> None:
750 """Set up a connection to our client.
751
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::
755
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.
759
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.
764
765 """
766 if new_client is not None:
767 if isinstance(new_client, (str, Pyro4.core.URI)):
768 self._client_client_client = Pyro4.Proxy(new_client)
769 else:
770 self._client_client_client = new_client
771 else:
772 self._client_client_client = None
773 # _client uses a setter. Log the result of assignment.
774 if self._client_client_client is None:
775 _logger.info("Current client is None.")
776 else:
777 _logger.info("Current client is %s.", str(self._client_client_client))
778
779 @keep_acquiring
780 def update_settings(self, settings, init: bool = False) -> None:
781 """Update settings, toggling acquisition if necessary."""
782 super().update_settings(settings, init)
783
784 # noinspection PyPep8Naming
785 def receiveClient(self, client_uri: str) -> None:
786 """A passthrough for compatibility."""
787 self.set_client(client_uri)
788
789 def grab_next_data(self, soft_trigger: bool = True):
790 """Returns results from next trigger via a direct call.
791
792 Args:
793 soft_trigger: calls :meth:`trigger` if `True`, waits for
794 hardware trigger if `False`.
795
796 """
797 if not self.enabledenabled:
798 raise microscope.DisabledDeviceError("Camera not enabled.")
799 self._new_data_condition.acquire()
800 # Push self onto client stack.
801 self.set_client(self)
802 # Wait for data from next trigger.
803 if soft_trigger:
804 self.trigger()
805 self._new_data_condition.wait()
806 # Pop self from client stack
807 self.set_client(None)
808 # Return the data.
809 return self._new_data
810
811 # noinspection PyPep8Naming
812 def receiveData(self, data, timestamp) -> None:
813 """Unblocks grab_next_frame so it can return."""
814 with self._new_data_condition:
815 self._new_data = (data, timestamp)
816 self._new_data_condition.notify()
817
818
820 """Adds functionality to :class:`DataDevice` to support cameras.
821
822 Defines the interface for cameras. Applies a transform to
823 acquired data in the processing step.
824
825 """
826
827 ALLOWED_TRANSFORMS = [p for p in itertools.product(*3 * [[False, True]])]
828
829 def __init__(self, **kwargs) -> None:
830 super().__init__(**kwargs)
831 # A list of readout mode descriptions.
832 self._readout_modes = ["default"]
833 # The index of the current readout mode.
834 self._readout_mode = 0
835 # Transforms to apply to data (fliplr, flipud, rot90)
836 # Transform to correct for readout order.
837 self._readout_transform = (False, False, False)
838 # Transform supplied by client to correct for system geometry.
839 self._client_transform = (False, False, False)
840 # Result of combining client and readout transforms
841 self._transform = (False, False, False)
842 # A transform provided by the client.
843 self.add_setting(
844 "transform",
845 "enum",
846 lambda: Camera.ALLOWED_TRANSFORMS.index(self._transform),
847 lambda index: self.set_transform(Camera.ALLOWED_TRANSFORMS[index]),
848 Camera.ALLOWED_TRANSFORMS,
849 )
850 self.add_setting(
851 "readout mode",
852 "enum",
853 lambda: self._readout_mode,
854 self.set_readout_mode,
855 lambda: self._readout_modes,
856 )
857 self.add_setting("roi", "tuple", self.get_roi, self.set_roi, None)
858
859 def _process_data(self, data):
860 """Apply self._transform to data."""
861 flips = (self._transform[0], self._transform[1])
862 rot = self._transform[2]
863
864 # Choose appropriate transform based on (flips, rot).
865 # Do rotation
866 data = numpy.rot90(data, rot)
867 # Flip
868 data = {
869 (0, 0): lambda d: d,
870 (0, 1): numpy.flipud,
871 (1, 0): numpy.fliplr,
872 (1, 1): lambda d: numpy.fliplr(numpy.flipud(d)),
873 }[flips](data)
874 return super()._process_data(data)
875
876 def set_readout_mode(self, description):
877 """Set the readout mode and _readout_transform."""
878 pass
879
880 def get_transform(self):
881 """Return the current transform without readout transform."""
882 return self._client_transform
883
884 def set_transform(self, transform):
885 """Combine provided transform with readout transform."""
886 if isinstance(transform, str):
887 transform = literal_eval(transform)
888 self._client_transform = transform
889 lr, ud, rot = (
890 self._readout_transform[i] ^ transform[i] for i in range(3)
891 )
892 if self._readout_transform[2] and self._client_transform[2]:
893 lr = not lr
894 ud = not ud
895 self._transform = (lr, ud, rot)
896
897 def _set_readout_transform(self, new_transform):
898 """Update readout transform and update resultant transform."""
899 self._readout_transform = [bool(int(t)) for t in new_transform]
901
902 @abc.abstractmethod
903 def set_exposure_time(self, value: float) -> None:
904 """Set the exposure time on the device in seconds."""
905 pass
906
907 def get_exposure_time(self) -> float:
908 """Return the current exposure time in seconds."""
909 pass
910
911 def get_cycle_time(self) -> float:
912 """Return the cycle time in seconds."""
913 pass
914
915 @abc.abstractmethod
916 def _get_sensor_shape(self) -> typing.Tuple[int, int]:
917 """Return a tuple of `(width, height)` indicating shape in pixels."""
918 pass
919
920 def get_sensor_shape(self) -> typing.Tuple[int, int]:
921 """Return a tuple of `(width, height)` corrected for transform."""
922 shape = self._get_sensor_shape()
923 if self._transform[2]:
924 # 90 degree rotation
925 shape = (shape[1], shape[0])
926 return shape
927
928 @abc.abstractmethod
929 def _get_binning(self) -> microscope.Binning:
930 """Return the current binning."""
931 pass
932
933 def get_binning(self) -> microscope.Binning:
934 """Return the current binning corrected for transform."""
935 binning = self._get_binning()
936 if self._transform[2]:
937 # 90 degree rotation
938 binning = microscope.Binning(binning[1], binning[0])
939 return binning
940
941 @abc.abstractmethod
942 def _set_binning(self, binning: microscope.Binning):
943 """Set binning along both axes. Return `True` if successful."""
944 pass
945
946 def set_binning(self, binning: microscope.Binning) -> None:
947 """Set binning along both axes. Return `True` if successful."""
948 h_bin, v_bin = binning
949 if self._transform[2]:
950 # 90 degree rotation
951 binning = microscope.Binning(v_bin, h_bin)
952 else:
953 binning = microscope.Binning(h_bin, v_bin)
954 return self._set_binning(binning)
955
956 @abc.abstractmethod
957 def _get_roi(self) -> microscope.ROI:
958 """Return the ROI as it is on hardware."""
959 raise NotImplementedError()
960
961 def get_roi(self) -> microscope.ROI:
962 """Return current ROI."""
963 roi = self._get_roi()
964 if self._transform[2]:
965 # 90 degree rotation
966 roi = microscope.ROI(roi[1], roi[0], roi[3], roi[2])
967 return roi
968
969 @abc.abstractmethod
970 def _set_roi(self, roi: microscope.ROI):
971 """Set the ROI on the hardware. Return `True` if successful."""
972 return False
973
974 def set_roi(self, roi: microscope.ROI) -> None:
975 """Set the ROI according to the provided rectangle.
976
977 Return True if ROI set correctly, False otherwise.
978 """
979 maxw, maxh = self.get_sensor_shape()
980 binning = self.get_binning()
981 left, top, width, height = roi
982 if not width: # 0 or None
983 width = maxw // binning.h
984 if not height: # 0 or None
985 height = maxh // binning.v
986 if self._transform[2]:
987 roi = microscope.ROI(left, top, height, width)
988 else:
989 roi = microscope.ROI(left, top, width, height)
990 return self._set_roi(roi)
991
992
993class SerialDeviceMixin(metaclass=abc.ABCMeta):
994 """Mixin for devices that are controlled via serial.
995
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.
1000
1001 Currently handles the flushing and locking of the comms channel
1002 until a command has finished, and the passthrough to the serial
1003 channel.
1004
1005 """
1006
1007 def __init__(self, **kwargs):
1008 super().__init__(**kwargs)
1009 # TODO: We should probably construct the connection here but
1010 # the Serial constructor takes a lot of arguments, and
1011 # it becomes tricky to separate those from arguments to
1012 # the constructor of other parent classes.
1013 self.connection = None # serial.Serial (to be constructed by child)
1014 self._comms_lock = threading.RLock()
1015
1016 def _readline(self) -> bytes:
1017 """Read a line from connection without leading and trailing whitespace."""
1018 return self.connection.readline().strip()
1019
1020 def _write(self, command: bytes) -> int:
1021 """Send a command to the device.
1022
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.
1026 """
1027 return self.connection.write(command + b"\r\n")
1028
1029 @staticmethod
1030 def lock_comms(func):
1031 """Decorator to flush input buffer and lock communications.
1032
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
1038 another can run.
1039
1040 """
1041
1042 @functools.wraps(func)
1043 def wrapper(self, *args, **kwargs):
1044 with self._comms_lock:
1045 self.connection.flushInput()
1046 return func(self, *args, **kwargs)
1047
1048 return wrapper
1049
1050
1051class DeformableMirror(TriggerTargetMixin, Device, metaclass=abc.ABCMeta):
1052 """Base class for Deformable Mirrors.
1053
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.
1063
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.
1068
1069 The private properties `_patterns` and `_pattern_idx` are
1070 initialized to `None` to support the queueing of patterns and
1071 software triggering.
1072
1073 """
1074
1075 @abc.abstractmethod
1076 def __init__(self, **kwargs) -> None:
1077 super().__init__(**kwargs)
1078 self._patterns: typing.Optional[numpy.ndarray] = None
1079 self._pattern_idx: int = -1
1080
1081 @property
1082 @abc.abstractmethod
1083 def n_actuators(self) -> int:
1084 raise NotImplementedError()
1085
1086 def _validate_patterns(self, patterns: numpy.ndarray) -> None:
1087 """Validate the shape of a series of patterns.
1088
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.
1094
1095 """
1096 if patterns.ndim > 2:
1097 raise ValueError(
1098 "PATTERNS has %d dimensions (must be 1 or 2)" % patterns.ndim
1099 )
1100 elif patterns.shape[-1] != self.n_actuators:
1101 raise ValueError(
1102 (
1103 "PATTERNS length of second dimension '%d' differs"
1104 " from number of actuators '%d'"
1105 % (patterns.shape[-1], self.n_actuators)
1106 )
1107 )
1108
1109 @abc.abstractmethod
1110 def _do_apply_pattern(self, pattern: numpy.ndarray) -> None:
1111 raise NotImplementedError()
1112
1113 def apply_pattern(self, pattern: numpy.ndarray) -> None:
1114 """Apply this pattern.
1115
1116 Raises:
1117 microscope.IncompatibleStateError: if device trigger type is
1118 not set to software.
1119
1120 """
1121 if self.trigger_type is not microscope.TriggerType.SOFTWARE:
1122 # An alternative to error is to change the trigger type,
1123 # apply the pattern, then restore the trigger type, but
1124 # that would clear the queue on the device. It's better
1125 # to have the user specifically do it. See issue #61.
1127 "apply_pattern requires software trigger type"
1128 )
1129 self._validate_patterns(pattern)
1130 self._do_apply_pattern(pattern)
1131
1132 def queue_patterns(self, patterns: numpy.ndarray) -> None:
1133 """Send values to the mirror.
1134
1135 Args:
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.
1139
1140 A convenience fallback is provided for software triggering is
1141 provided.
1142
1143 """
1144 self._validate_patterns(patterns)
1145 self._patterns = patterns
1146 self._pattern_idx = -1 # none is applied yet
1147
1148 def next_pattern(self) -> None:
1149 """Apply the next pattern in the queue.
1150
1151 DEPRECATED: this is the same as calling :meth:`trigger`.
1152
1153 """
1154 self.triggertrigger()
1155
1156 def _do_trigger(self) -> None:
1157 """Convenience fallback.
1158
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.
1163
1164 Devices that support queuing patterns, should override this
1165 method.
1166
1167 .. todo::
1168
1169 Instead of a convenience fallback, we should have a
1170 separate mixin for this.
1171
1172 """
1173 if self._patterns is None:
1174 raise microscope.DeviceError("no pattern queued to apply")
1175 self._pattern_idx += 1
1176 self.apply_pattern(self._patterns[self._pattern_idx, :])
1177
1178 def trigger(self) -> None:
1179 """Apply the next pattern in the queue."""
1180 # This is just a passthrough to the TriggerTargetMixin class
1181 # and only exists for the docstring.
1182 return super().trigger()
1183
1184
1185class LightSource(TriggerTargetMixin, Device, metaclass=abc.ABCMeta):
1186 """Light source such as lasers or LEDs.
1187
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.
1193
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.
1197
1198 """
1199
1200 @abc.abstractmethod
1201 def __init__(self, **kwargs):
1202 super().__init__(**kwargs)
1203 self._set_point = 0.0
1204
1205 @abc.abstractmethod
1206 def get_status(self) -> typing.List[str]:
1207 """Query and return the light source status."""
1208 result = []
1209 return result
1210
1211 @abc.abstractmethod
1212 def get_is_on(self) -> bool:
1213 """Return True if the light source is currently able to produce light."""
1214 pass
1215
1216 @abc.abstractmethod
1217 def _do_get_power(self) -> float:
1218 """Internal function that actually returns the light source power."""
1219 raise NotImplementedError()
1220
1221 @abc.abstractmethod
1222 def _do_set_power(self, power: float) -> None:
1223 """Internal function that actually sets the light source power.
1224
1225 This function will be called by the `power` attribute setter
1226 after clipping the argument to the [0, 1] interval.
1227
1228 """
1229 raise NotImplementedError()
1230
1231 @property
1232 def power(self) -> float:
1233 """Light source power in the [0, 1] interval."""
1234 return self._do_get_power()
1235
1236 @power.setter
1237 def power(self, power: float) -> None:
1238 """Light source power in the [0, 1] interval.
1239
1240 The power value will be clipped to [0, 1] interval.
1241 """
1242 clipped_power = max(min(power, 1.0), 0.0)
1243 self._do_set_power(clipped_power)
1244 self._set_point = clipped_power
1245
1246 def get_set_power(self) -> float:
1247 """Return the power set point."""
1248 return self._set_point
1249
1250
1251class FilterWheel(Device, metaclass=abc.ABCMeta):
1252 """ABC for filter wheels, cube turrets, and filter sliders.
1253
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
1257 filter.
1258
1259 Args:
1260 positions: total number of filter positions on this device.
1261
1262 """
1263
1264 def __init__(self, positions: int, **kwargs) -> None:
1265 super().__init__(**kwargs)
1266 if positions < 1:
1267 raise ValueError(
1268 "positions must be a positive number (was %d)" % positions
1269 )
1270 self._positions = positions
1271 # The position as an integer.
1272 # Deprecated: clients should call get_position and set_position;
1273 # still exposed as a setting until cockpit uses set_position.
1274 self.add_setting(
1275 "position",
1276 "int",
1277 self.get_position,
1278 self.set_position,
1279 lambda: (0, self.get_num_positions()),
1280 )
1281
1282 @property
1283 def n_positions(self) -> int:
1284 """Number of wheel positions."""
1285 return self._positions
1286
1287 @property
1288 def position(self) -> int:
1289 """Number of wheel positions (zero-based)."""
1290 return self._do_get_position()
1291
1292 @position.setter
1293 def position(self, new_position: int) -> None:
1294 if 0 <= new_position < self.n_positions:
1295 return self._do_set_position(new_position)
1296 else:
1297 raise ValueError(
1298 "can't move to position %d, limits are [0 %d]"
1299 % (new_position, self.n_positions - 1)
1300 )
1301
1302 @abc.abstractmethod
1303 def _do_get_position(self) -> int:
1304 raise NotImplementedError()
1305
1306 @abc.abstractmethod
1307 def _do_set_position(self, position: int) -> None:
1308 raise NotImplementedError()
1309
1310 # Deprecated and kept for backwards compatibility.
1311 def get_num_positions(self) -> int:
1312 """Deprecated, use the `n_positions` property."""
1313 return self.n_positions
1314
1315 def get_position(self) -> int:
1316 return self.positionpositionposition
1317
1318 def set_position(self, position: int) -> None:
1319 self.positionpositionposition = position
1320
1321
1322class Controller(Device, metaclass=abc.ABCMeta):
1323 """Device that controls multiple devices.
1324
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.
1328
1329 Each of the controlled devices requires a name. The choice of
1330 name and its documentation is left to the concrete class.
1331
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.
1337
1338 """
1339
1340 @property
1341 @abc.abstractmethod
1342 def devices(self) -> typing.Mapping[str, Device]:
1343 """Map of names to the controlled devices."""
1344 raise NotImplementedError()
1345
1346 def _do_shutdown(self) -> None:
1347 for d in self.devices.values():
1348 d.shutdown()
1349
1350
1351class StageAxis(metaclass=abc.ABCMeta):
1352 """A single dimension axis for a :class:`StageDevice`.
1353
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.
1358
1359 The interface for `StageAxis` maps to that of `StageDevice` so
1360 refer to its documentation.
1361
1362 """
1363
1364 @abc.abstractmethod
1365 def move_by(self, delta: float) -> None:
1366 """Move axis by given amount."""
1367 raise NotImplementedError()
1368
1369 @abc.abstractmethod
1370 def move_to(self, pos: float) -> None:
1371 """Move axis to specified position."""
1372 raise NotImplementedError()
1373
1374 @property
1375 @abc.abstractmethod
1376 def position(self) -> float:
1377 """Current axis position."""
1378 raise NotImplementedError()
1379
1380 @property
1381 @abc.abstractmethod
1382 def limits(self) -> microscope.AxisLimits:
1383 """Upper and lower limits values for position."""
1384 raise NotImplementedError()
1385
1386
1387class Stage(Device, metaclass=abc.ABCMeta):
1388 """A stage device, composed of :class:`StageAxis` instances.
1389
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"`.
1395
1396 .. code-block:: python
1397
1398 stage = SomeStageDevice()
1399 stage.enable() # may trigger a stage move
1400
1401 # move operations
1402 stage.move_to({'x': 42.0, 'y': -5.1})
1403 stage.move_by({'x': -5.3, 'y': 14.6})
1404
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)
1410
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.
1415
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
1420 axes, like so:
1421
1422 .. code-block:: python
1423
1424 # Move the x axis first, then mvoe the y axis:
1425 stage.move_by({'x': 10})
1426 stage.move_by({'y': 4})
1427
1428 # The same thing but via the individual axes:
1429 stage.axes['x'].move_by(10)
1430 stage.axes['y'].move_by(4)
1431
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.
1436
1437 .. code-block:: python
1438
1439 # Moves x axis to the its upper limit:
1440 x_axis.move_to(x_axis.limits.upper)
1441
1442 # The same as above since the move operations are clipped to
1443 # the axes limits automatically.
1444 import math
1445 x_axis.move_to(math.inf)
1446 x_axis.move_by(math.inf)
1447
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`).
1451 """
1452
1453 @property
1454 @abc.abstractmethod
1455 def axes(self) -> typing.Mapping[str, StageAxis]:
1456 """Map of axis names to the corresponding :class:`StageAxis`.
1457
1458 .. code-block:: python
1459
1460 for name, axis in stage.axes.items():
1461 print(f'moving axis named {name}')
1462 axis.move_by(1)
1463
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.
1468 """
1469 raise NotImplementedError()
1470
1471
1472 @abc.abstractmethod
1473 def may_move_on_enable(self) -> bool:
1474 """Whether calling :func:`enable` is likely to make the stage move.
1475
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".
1479
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.
1485
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`.
1494
1495 """
1496 raise NotImplementedError()
1497
1498
1499 @property
1500 def position(self) -> typing.Mapping[str, float]:
1501 """Map of axis name to their current position.
1502
1503 .. code-block:: python
1504
1505 for name, position in stage.position.items():
1506 print(f'{name} axis is at position {position}')
1507
1508 The units of the position is the same as the ones being
1509 currently used for the absolute move (:func:`move_to`)
1510 operations.
1511 """
1512 return {name: axis.position for name, axis in self.axes.items()}
1513
1514 @property
1515 def limits(self) -> typing.Mapping[str, microscope.AxisLimits]:
1516 """Map of axis name to its upper and lower limits.
1517
1518 .. code-block:: python
1519
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}')
1523
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.
1527
1528 The units of the limits is the same as the ones being
1529 currently used for the move operations.
1530
1531 """
1532 return {name: axis.limits for name, axis in self.axes.items()}
1533
1534 @abc.abstractmethod
1535 def move_by(self, delta: typing.Mapping[str, float]) -> None:
1536 """Move axes by the corresponding amounts.
1537
1538 Args:
1539 delta: map of axis name to the amount to be moved.
1540
1541 .. code-block:: python
1542
1543 # Move 'x' axis by 10.2 units and the y axis by -5 units:
1544 stage.move_by({'x': 10.2, 'y': -5})
1545
1546 # The above is equivalent, but possibly faster than:
1547 stage.axes['x'].move_by(10.2)
1548 stage.axes['y'].move_by(-5)
1549
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.
1553
1554 """
1555 # TODO: implement a software fallback that moves the
1556 # individual axis, for stages that don't have provide
1557 # simultaneous move of multiple axes.
1558 raise NotImplementedError()
1559
1560 @abc.abstractmethod
1561 def move_to(self, position: typing.Mapping[str, float]) -> None:
1562 """Move axes to the corresponding positions.
1563
1564 Args:
1565 position: map of axis name to the positions to move to.
1566
1567 .. code-block:: python
1568
1569 # Move 'x' axis to position 8 and the y axis to position -5.3
1570 stage.move_to({'x': 8, 'y': -5.3})
1571
1572 # The above is equivalent to
1573 stage.axes['x'].move_to(8)
1574 stage.axes['y'].move_to(-5.3)
1575
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.
1579
1580 """
1581 raise NotImplementedError()
bool readonly(self)
Definition: abc.py:161
def values(self)
Definition: abc.py:173
None set(self, value)
Definition: abc.py:164
None set_binning(self, microscope.Binning binning)
Definition: abc.py:946
float get_exposure_time(self)
Definition: abc.py:907
float get_cycle_time(self)
Definition: abc.py:911
microscope.ROI get_roi(self)
Definition: abc.py:961
def _set_roi(self, microscope.ROI roi)
Definition: abc.py:970
typing.Tuple[int, int] get_sensor_shape(self)
Definition: abc.py:920
None set_roi(self, microscope.ROI roi)
Definition: abc.py:974
microscope.ROI _get_roi(self)
Definition: abc.py:957
None set_exposure_time(self, float value)
Definition: abc.py:903
def set_transform(self, transform)
Definition: abc.py:884
None __init__(self, **kwargs)
Definition: abc.py:829
def get_transform(self)
Definition: abc.py:880
def _set_binning(self, microscope.Binning binning)
Definition: abc.py:942
def set_readout_mode(self, description)
Definition: abc.py:876
microscope.Binning get_binning(self)
Definition: abc.py:933
typing.Mapping[str, Device] devices(self)
Definition: abc.py:1342
def _client(self, val)
Definition: abc.py:737
None _fetch_loop(self)
Definition: abc.py:710
None disable(self)
Definition: abc.py:629
None update_settings(self, settings, bool init=False)
Definition: abc.py:780
None abort(self)
Definition: abc.py:588
None _dispatch_loop(self)
Definition: abc.py:684
None receiveData(self, data, timestamp)
Definition: abc.py:812
None _put(self, data, timestamp)
Definition: abc.py:745
None set_client(self, new_client)
Definition: abc.py:749
def _send_data(self, client, data, timestamp)
Definition: abc.py:660
def _process_data(self, data)
Definition: abc.py:656
def _client(self)
Definition: abc.py:732
None receiveClient(self, str client_uri)
Definition: abc.py:785
None enable(self)
Definition: abc.py:592
def grab_next_data(self, bool soft_trigger=True)
Definition: abc.py:789
None _fetch_data(self)
Definition: abc.py:643
None __init__(self, int buffer_length=0, **kwargs)
Definition: abc.py:558
None _validate_patterns(self, numpy.ndarray patterns)
Definition: abc.py:1086
None queue_patterns(self, numpy.ndarray patterns)
Definition: abc.py:1132
None apply_pattern(self, numpy.ndarray pattern)
Definition: abc.py:1113
None _do_apply_pattern(self, numpy.ndarray pattern)
Definition: abc.py:1110
None add_setting(self, name, dtype, get_func, set_func, values, typing.Optional[typing.Callable[[], bool]] readonly=None)
Definition: abc.py:407
def _do_disable(self)
Definition: abc.py:298
def get_all_settings(self)
Definition: abc.py:455
def get_setting(self, str name)
Definition: abc.py:447
def _do_enable(self)
Definition: abc.py:312
None _do_shutdown(self)
Definition: abc.py:329
None shutdown(self)
Definition: abc.py:349
def describe_settings(self)
Definition: abc.py:480
def update_settings(self, incoming, bool init=False)
Definition: abc.py:484
None set_setting(self, str name, value)
Definition: abc.py:468
None enable(self)
Definition: abc.py:321
None disable(self)
Definition: abc.py:307
def describe_setting(self, str name)
Definition: abc.py:476
int get_num_positions(self)
Definition: abc.py:1311
int get_position(self)
Definition: abc.py:1315
None set_position(self, int position)
Definition: abc.py:1318
int _do_get_position(self)
Definition: abc.py:1303
None _do_set_position(self, int position)
Definition: abc.py:1307
None position(self, int new_position)
Definition: abc.py:1293
int n_positions(self)
Definition: abc.py:1283
float get_set_power(self)
Definition: abc.py:1246
bool get_is_on(self)
Definition: abc.py:1212
float power(self)
Definition: abc.py:1232
float _do_get_power(self)
Definition: abc.py:1217
None _do_set_power(self, float power)
Definition: abc.py:1222
typing.List[str] get_status(self)
Definition: abc.py:1206
None move_to(self, float pos)
Definition: abc.py:1370
None move_by(self, float delta)
Definition: abc.py:1365
microscope.AxisLimits limits(self)
Definition: abc.py:1382
float position(self)
Definition: abc.py:1376
typing.Mapping[str, microscope.AxisLimits] limits(self)
Definition: abc.py:1515
None move_by(self, typing.Mapping[str, float] delta)
Definition: abc.py:1535
typing.Mapping[str, StageAxis] axes(self)
Definition: abc.py:1455
bool may_move_on_enable(self)
Definition: abc.py:1473
None move_to(self, typing.Mapping[str, float] position)
Definition: abc.py:1561
typing.Mapping[str, float] position(self)
Definition: abc.py:1500
microscope.TriggerType trigger_type(self)
Definition: abc.py:243
None set_trigger(self, microscope.TriggerType ttype, microscope.TriggerMode tmode)
Definition: abc.py:249