Code source de datalab.plugins.instruments
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""Instruments exposed by application plugins as tools.
An instrument is a headless object: it holds settings (a guidata DataSet),
returns a live frame for these settings, and acquires objects. DataLab shows
it in a window with the live view on the left and the settings on the right.
DataLab Desktop and DataLab-Web share this module.
"""
from __future__ import annotations
import abc
import dataclasses
import math
from collections.abc import Sequence
import guidata.dataset as gds
from sigima.objects import ImageObj, SignalObj
__all__ = ["InstrumentAcquisition", "InstrumentFrame", "PluginInstrument"]
def _validate_objects(objects: Sequence[SignalObj | ImageObj], name: str) -> tuple:
"""Return objects as a tuple of signals or of images."""
objects = tuple(objects)
if not objects:
raise ValueError(f"{name} must contain at least one object")
if not (
all(isinstance(obj, SignalObj) for obj in objects)
or all(isinstance(obj, ImageObj) for obj in objects)
):
raise TypeError(f"{name} must contain signals only or images only")
return objects
[docs]
@dataclasses.dataclass(frozen=True)
class InstrumentFrame:
"""Live frame shown by DataLab for the current instrument settings.
Args:
objects: signals drawn together, or a single image
summary: short text shown below the view (e.g. measured levels)
value_range: fixed Y range of signals, or color range of the image
(``None``: automatic)
"""
objects: Sequence[SignalObj | ImageObj]
summary: str = ""
value_range: tuple[float, float] | None = None
def __post_init__(self) -> None:
"""Validate the frame contents."""
objects = _validate_objects(self.objects, "Instrument frame")
if isinstance(objects[0], ImageObj) and len(objects) > 1:
raise ValueError("Instrument frame must contain a single image")
object.__setattr__(self, "objects", objects)
if not isinstance(self.summary, str):
raise TypeError("Instrument frame summary must be a string")
if self.value_range is not None:
low, high = (float(value) for value in self.value_range)
if not (math.isfinite(low) and math.isfinite(high) and low < high):
raise ValueError("Instrument frame range must be finite and increasing")
object.__setattr__(self, "value_range", (low, high))
[docs]
@dataclasses.dataclass(frozen=True)
class InstrumentAcquisition:
"""Objects acquired by an instrument, added by DataLab to the workspace.
DataLab adds them to a new group, or to the current group of the panel when
the user turns off the **New group** option of the instrument window (see
:attr:`PluginInstrument.new_group`).
Args:
group_title: title of the group created for the acquisition, if any
objects: acquired signals, or acquired images
"""
group_title: str
objects: Sequence[SignalObj | ImageObj]
def __post_init__(self) -> None:
"""Validate the acquisition contents."""
if not isinstance(self.group_title, str) or not self.group_title.strip():
raise ValueError("Instrument acquisition group title must not be empty")
object.__setattr__(
self, "objects", _validate_objects(self.objects, "Instrument acquisition")
)
[docs]
class PluginInstrument(abc.ABC):
"""Instrument opened by a plugin tool.
DataLab writes the edited values into :attr:`settings` before calling
:meth:`preview` or :meth:`acquire`. Both methods raise ``ValueError``
with a user-facing message when the settings cannot be used. The plugin
keeps one instance per tool during the session, so settings are kept
from one opening to the next.
DataLab calls :meth:`open` when the instrument window opens and
:meth:`close` when it closes (including when the plugin is reloaded or
DataLab quits). It calls these methods, :meth:`preview` and :meth:`acquire`
one at a time, outside the GUI thread, even when the window is closed and
opened again while a call is running, and writes :attr:`settings` only
between two calls: a slow instrument does not freeze DataLab. Data received
by the instrument in its own threads (e.g. device events) must be stored
for the next :meth:`preview` or :meth:`acquire` call.
Args:
settings: instrument settings, edited by DataLab
"""
#: Interval between two frames in live mode, in milliseconds
live_interval_ms: int = 500
#: Default state of the **New group** option of the instrument window (if
#: True, each acquisition is added to a new group, otherwise to the current
#: group of the panel). The choice of the user, once made, prevails.
new_group: bool = True
def __init__(self, settings: gds.DataSet) -> None:
if not isinstance(settings, gds.DataSet):
raise TypeError("Instrument settings must be a guidata DataSet")
self.settings = settings
[docs]
def open(self) -> None:
"""Prepare the instrument when its window opens (e.g. connect to it).
Raises ``ValueError`` with a user-facing message if the instrument
cannot be used: DataLab shows this message, and does not call
:meth:`preview` or :meth:`acquire` until a new request (e.g. after a
settings change) opens the instrument successfully. Does nothing by
default.
"""
[docs]
def close(self) -> None:
"""Release the instrument when its window closes (e.g. disconnect).
Called even if :meth:`open` failed, and possibly several times: it must
release only what is still held. Does nothing by default.
"""
[docs]
@abc.abstractmethod
def preview(self) -> InstrumentFrame:
"""Return a live frame for the current settings."""
[docs]
@abc.abstractmethod
def acquire(self) -> InstrumentAcquisition:
"""Acquire objects with the current settings."""