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.
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)