Custom Acquisitions

Custom Acquisitions#

General Guidelines#

It is possible to create custom acquisition classes that can be used with the LDAQ library. This is done by subclassing the LDAQ.acquisition_base.BaseAcquisition class and implementing the abstract methods. The following is a list of guidelines that should be followed when creating custom acquisition classes:

class LDAQ.acquisition_base.BaseAcquisition[source]

Parent acquisition class that should be used when creating new child acquisition source class. Child class should override methods the following methods:

  • self.__init__()

  • self.set_data_source()

  • self.terminate_data_source()

  • self.read_data()

  • self.clear_buffer() (optional)

  • self.get_sample_rate() (optional)

For further information on how to override these methods, see the listed methods docstrings.

Additionally, the __init__() or set_data_source() methods should override or be able to set the following attributes:

  • self._channel_names_init - list of original data channels names from source

  • self._channel_names_video_init - list of original video channels names from source

  • self._channel_shapes_video_init - list of original video channels shapes from source

  • self.sample_rate = 0 - sample rate of acquisition source

clear_buffer() None[source]

EDIT in child class (Optional).

The source buffer should be cleared with this method. It can either clear the buffer, or just read the data with self.read_data() and does not add/save data anywhere. By default, this method will read the data from the source and not add/save data anywhere.

Returns None.

get_sample_rate() float[source]

EDIT in child class (Optional).

Returns sample rate of acquisition class.

read_data() ndarray[source]

EDIT in child class.

This method only reads data from the source and transforms data into standard format used by other methods. It is called within self.acquire() method which properly handles acquiring data and saves it into pyTrigger ring buffer.

Must ALWAYS return a 2D numpy array of shape (n_samples, n_columns).

IMPORTANT: If some of the channels are videos (2D array - so shape is (n_samples, n_pixels_width, n_pixels_height)), then data has to be reshaped to shape (n_samples, n_pixels_width*n_pixels_height). Then data from multiple sources have to be concatenated into one array of shape (n_samples, n_cols), where cols is the combined number of pixel of all video sources and number of channels in data sources.

For an example where data source has 2 video sources with resolution 300x200 and 2 data channels, the final shape returned by this methods should be (n_samples, 300*200*2+2).

For video sources, the shape of the video is automatically stored in self.channel_shapes_video_init when self.set_data_source() is called. When data is retrieved from the source, it is reshaped to (n_samples, n_pixels_width, n_pixels_height).

Returns:

2D numpy array of shape (n_samples, n_columns)

Return type:

data (np.ndarray)

terminate_data_source() None[source]

EDIT in child class.

Properly closes/disconnects acquisition source after the measurement. The method should be able to handle mutliple calls in a row.

Example#

In the example below, the source code of National Instrument acquisition class is shown.

Source code of National Instrument acquisition class#
  1from __future__ import annotations
  2
  3import numpy as np
  4
  5from ..acquisition_base import BaseAcquisition
  6
  7_NIDAQWRAPPER_AVAILABLE = False
  8try:
  9    from nidaqwrapper import AITask, get_task_by_name
 10    _NIDAQWRAPPER_AVAILABLE = True
 11except ImportError:
 12    pass
 13
 14
 15class NIAcquisition(BaseAcquisition):
 16    """Acquisition class for National Instruments devices via nidaqwrapper.
 17
 18    A thin wrapper around ``nidaqwrapper.AITask`` that satisfies the
 19    ``BaseAcquisition`` contract. Supports both programmatic tasks
 20    (``AITask`` objects) and tasks defined in NI MAX (task name strings).
 21
 22    Parameters
 23    ----------
 24    task : nidaqwrapper.AITask or str
 25        Either a fully configured ``AITask`` instance or the name of a task
 26        saved in NI MAX.  When a string is supplied the task is loaded via
 27        ``nidaqwrapper.get_task_by_name()``.
 28    acquisition_name : str or None, optional
 29        Human-readable name for this acquisition source.  Defaults to the
 30        underlying NI task name when ``None``.
 31
 32    Raises
 33    ------
 34    ImportError
 35        If the ``nidaqwrapper`` package is not installed.
 36    TypeError
 37        If ``task`` is not an ``AITask`` instance or a string.
 38
 39    Examples
 40    --------
 41    Wrap a programmatically created task:
 42
 43    >>> ai = AITask("my_task", sample_rate=10_000)
 44    >>> ai.add_channel(...)
 45    >>> acq = NIAcquisition(ai)
 46
 47    Load a task saved in NI MAX:
 48
 49    >>> acq = NIAcquisition("MyNIMaxTask")
 50    """
 51
 52    def __init__(
 53        self,
 54        task: AITask | str,
 55        acquisition_name: str | None = None,
 56    ) -> None:
 57        """
 58        Initialize NIAcquisition.
 59
 60        Parameters
 61        ----------
 62        task : nidaqwrapper.AITask or str
 63            Source task: either an ``AITask`` object or an NI MAX task name.
 64        acquisition_name : str or None, optional
 65            Name for this acquisition source.  Uses the task name when None.
 66
 67        Raises
 68        ------
 69        ImportError
 70            If ``nidaqwrapper`` is not installed.
 71        TypeError
 72            If ``task`` is neither an ``AITask`` nor a string.
 73        """
 74        if not _NIDAQWRAPPER_AVAILABLE:
 75            raise ImportError(
 76                "nidaqwrapper is not installed. "
 77                "Install it before using NIAcquisition."
 78            )
 79
 80        super().__init__()
 81
 82        if isinstance(task, AITask):
 83            self._ai_task: AITask = task
 84        elif isinstance(task, str):
 85            ni_task = get_task_by_name(task)
 86            if ni_task is None:
 87                raise ValueError(f"NI MAX task '{task}' not found.")
 88            self._ai_task = AITask.from_task(ni_task)
 89        else:
 90            raise TypeError(
 91                f"task must be an AITask instance or a string (NI MAX task name), "
 92                f"got {type(task).__name__!r}."
 93            )
 94
 95        self.acquisition_name = (
 96            acquisition_name if acquisition_name is not None else self._ai_task.task_name
 97        )
 98        self.sample_rate = self._ai_task.sample_rate
 99        self._channel_names_init = list(self._ai_task.channel_list)
100        self._task_active = False
101
102        self._set_all_channels()  # populate channel_names_all before set_trigger
103        self.set_trigger(1e20, 0, duration=1.0)
104
105    def set_data_source(self) -> None:
106        """Start the underlying AITask in preparation for acquisition.
107
108        Calls ``super().set_data_source()`` at the end to satisfy the
109        ``BaseAcquisition`` contract.
110        """
111        if self._task_active:
112            return
113
114        self._ai_task.start()
115        self._task_active = True
116        super().set_data_source()
117
118    def terminate_data_source(self) -> None:
119        """Stop the NIDAQmx task.
120
121        Safe to call multiple times; subsequent calls when the task is
122        already stopped are silently ignored.
123        """
124        if not self._task_active:
125            return
126
127        self._ai_task.task.stop()
128        self._task_active = False
129
130    def read_data(self) -> np.ndarray:
131        """Read all currently available samples from the device buffer.
132
133        Returns
134        -------
135        np.ndarray
136            2-D array of shape ``(n_samples, n_channels)``.  Returns an
137            empty array of shape ``(0, n_channels)`` when no data is
138            available.
139        """
140        n_channels = len(self._channel_names_init)
141
142        raw = self._ai_task.acquire(n_samples=None)
143
144        if raw is None or raw.size == 0:
145            return np.empty((0, n_channels))
146
147        return raw
148
149    def clear_buffer(self) -> None:
150        """Read and discard all data currently in the device buffer."""
151        self._ai_task.acquire(n_samples=None)