Detector

Creating a Detector Instance

A detector instance is created by subclassing MultiDetectorInterface, which provides essential features for a multi-dimensional detector.

For real hardware, you should implement device-specific communication methods in your subclass as follows:

from lys_instr import MultiDetectorInterface

class YourDetector(MultiDetectorInterface):       # Give a name to your subclass, e.g., ``YourDetector``

    def __init__(self, indexShape=(), frameShape=(), exposure=None, **kwargs):
        super().__init__(**kwargs)
        ... your code to establish connection and initialize the instruments ...
        self.exposure = exposure
        self.start()

    def _run(self, iter=1):
        ... your code to tell the instrument to trigger data acquisition ...

    def _get(self):
        ... your code to read the acquired data from the instrument ...
        return ... a dictionary with frame indices as keys and NumPy ndarrays of the acquired data as values ...

    def _stop(self):
        ... your code to tell the instrument to stop ongoing acquisition ...

    def _isAlive(self):
        ... your code to check if the detector is connected and functioning ...
        return ... True if alive, False if not ...

    @property
    def frameShape(self):
        ... your code to specify the shape of each data frame ...
        return ... a tuple of integers specifying the shape of each data frame ...

    @property
    def indexShape(self):
        ... your code to specify the shape of frame indices ...
        return ... a tuple of integers specifying the shape of frame indices ...

    @property
    def axes(self):
        ... your code to specify the physical axis values for each dimension ...
        return ... a list of arrays or lists, one for each axis, in the order [index axes..., frame axes...]
        # E.g., [listForIndexAxis1, listForIndexAxis2, ..., listForFrameAxis1, listForFrameAxis2, ...]

frameShape specifies the shape of each frame (for example, (256, 256) for a 256×256 image). indexShape specifies the shape of frame indices (for example, (10,) for 10 frames in a single run with indices 0 to 9).

For example, to acquire 10 images of size 256×256 while sweeping a motor over 10 steps, set indexShape=(10,) and frameShape=(256, 256). If no indexing is needed, simply omit indexShape or set indexShape=() (that is, a single frame per run).

See Detector GUI for displaying non-two-dimensional frames.

Step-by-Step Demonstration

Here we illustrate step-by-step construction of YourDetector for a dummy device that provides pre-encoded or synthetic data frames.

Subclass MultiDetectorInterface to create YourDetector. The data-supply logic is delegated to the setData(), which keeps an internal buffer _data of acquired frames for use by other methods.

from lys_instr import MultiDetectorInterface

class YourDetector(MultiDetectorInterface):

    def __init__(self, indexShape=(), frameShape=(), exposure=None, **kwargs):
        super().__init__(**kwargs)

        self.setData(indexShape=indexShape, frameShape=frameShape)
        self.error = False

        self.exposure = exposure
        self.start()

    def setData(self, data=None, indexShape=None, frameShape=None):
        if data is None:
            from lys_instr.dummy.MultiDetector import RandomData
            self._obj = RandomData(indexShape, frameShape)
        else:
            self._obj = data
        self._data = {}

By default, setData() generates random noise with the specified indexShape and frameShape. It can also load pre-encoded frames via the GUI by user selection. For real hardware, the details of the dummy data generation logic can be ignored.

Implement _run() to sequentially fetch or generate frames, populate _data for respective indices, emit updated signal after each frame, and check _shouldStop to allow cancellation.

def _run(self, iter=1):
    self._shouldStop = False

    i = 0
    while i != iter:
        for idx, data in self._obj:
            if self._shouldStop:
                return
            import time
            time.sleep(self.exposure * self._obj.nframes)
            self._data[idx] = data
            self.updated.emit()
        i += 1

Implement _stop() to request acquisition cancellation by setting a flag checked by _run().

def _stop(self):
    self._shouldStop = True

Implement _get() to return the acquired frames in _data and clear the buffer afterward.

def _get(self):
    data = self._data.copy()
    self._data.clear()
    return data

Implement _isAlive() to report the connection status of the device, here managed by an internal error flag.

def _isAlive(self):
    return not self.error

Implement frameShape, indexShape, and axes properties to return the corresponding attributes set in __init__(). (In this example, the properties delegate to the dummy-data object self._obj (a RandomData instance); you can instead implement them explicitly as needed.)

@property
def frameShape(self):
    return self._obj.frameShape

@property
def indexShape(self):
    return self._obj.indexShape

@property
def axes(self):
    return self._obj.axes

Optionally, implement settingsWidget to return a QWidget for later use by GUI. The _OptionalPanel class in the lys_instr.dummy.MultiDetector module can readily be used.

def settingsWidget(self):
    from lys_instr.dummy.MultiDetector import _OptionalPanel
    return _OptionalPanel(self)

The class constructed above is actually the MultiDetectorDummy class provided in the lys_instr.dummy module.

Checking Operations

To verify functionality, instantiate your detector class, for example, YourDetector. (Import it if defined in a separate module.)

detector = YourDetector(... your parameters ...)

For demonstration, we use the YourDetector class defined above with indexShape=(), frameShape=(256, 256), and 0.1 seconds exposure time:

detector = YourDetector(frameShape=(256, 256), exposure=0.1)

This is functionally equivalent to instantiating the provided MultiDetectorDummy class:

from lys_instr import dummy

detector = dummy.MultiDetectorDummy(frameShape=(256, 256))

Now, you can use the startAcq(), stop(), isBusy(), and isAlive() methods provided by MultiDetectorInterface to confirm that the detector is functioning correctly. For example:

import time

data = detector.startAcq(wait=True, output=True)   # Start acquisition of 1 frame
print(data)     # Returns a dictionary, e.g., {(): array([[0.1, 0.2, ...], [...], ...])}