diff --git a/tests/component/readers/__init__.py b/tests/component/readers/__init__.py new file mode 100644 index 000000000..c8e805921 --- /dev/null +++ b/tests/component/readers/__init__.py @@ -0,0 +1 @@ +"""Stream readers test module.""" diff --git a/tests/component/readers/conftest.py b/tests/component/readers/conftest.py new file mode 100644 index 000000000..7c53572e3 --- /dev/null +++ b/tests/component/readers/conftest.py @@ -0,0 +1,527 @@ +"""Shared fixtures and utilities for component tests.""" + +from __future__ import annotations + +from datetime import timezone +from typing import Callable, TypeVar + +import numpy +import pytest +from hightime import datetime as ht_datetime +from nitypes.waveform import DigitalWaveform + +import nidaqmx +import nidaqmx.system +from nidaqmx.constants import AcquisitionType, LineGrouping +from nidaqmx.utils import flatten_channel_string + + +# Simulated DAQ voltage data is a noisy sinewave within the range of the minimum and maximum values +# of the virtual channel. We can leverage this behavior to validate we get the correct data from +# the Python bindings. +def _get_voltage_offset_for_chan(chan_index: int) -> float: + return float(chan_index + 1) + + +def _get_voltage_setpoint_for_chan(chan_index: int) -> float: + return float(chan_index + 1) + + +def _get_current_setpoint_for_chan(chan_index: int) -> float: + return float(chan_index + 1) + + +def _volts_to_codes(volts: float, max_code: int = 32767, max_voltage: float = 10.0) -> int: + return int(volts * max_code / max_voltage) + + +def _pwr_volts_to_codes(volts: float, codes_per_volt: int = 4096) -> int: + return int(volts * codes_per_volt) + + +def _pwr_current_to_codes(current: float, codes_per_amp: int = 8192) -> int: + return int(current * codes_per_amp) + + +def _get_voltage_code_setpoint_for_chan(chan_index: int) -> int: + return _pwr_volts_to_codes(_get_voltage_setpoint_for_chan(chan_index)) + + +def _get_current_code_setpoint_for_chan(chan_index: int) -> int: + return _pwr_current_to_codes(_get_current_setpoint_for_chan(chan_index)) + + +# Note: Since we only use positive voltages, this works fine for both signed and unsigned reads. +def _get_voltage_code_offset_for_chan(chan_index: int) -> int: + voltage_limits = _get_voltage_offset_for_chan(chan_index) + return _volts_to_codes(voltage_limits) + + +def _get_num_lines_in_task(task: nidaqmx.Task) -> int: + return sum([chan.di_num_lines for chan in task.channels]) + + +def _get_expected_digital_data_for_sample(num_lines: int, sample_number: int) -> int: + result = 0 + # Simulated digital signals "count" from 0 in binary within each group of 8 lines. + for _ in range((num_lines + 7) // 8): + result = (result << 8) | sample_number + + line_mask = (2**num_lines) - 1 + return result & line_mask + + +def _get_expected_data_for_line(num_samples: int, line_number: int) -> list[int]: + data = [] + # Simulated digital signals "count" from 0 in binary within each group of 8 lines. + # Each line represents a bit in the binary representation of the sample number. + # - line 0 represents bit 0 (LSB) - alternates every sample: 0,1,0,1,0,1,0,1... + # - line 1 represents bit 1 - alternates every 2 samples: 0,0,1,1,0,0,1,1... + # - line 2 represents bit 2 - alternates every 4 samples: 0,0,0,0,1,1,1,1... + line_number %= 8 + for sample_num in range(num_samples): + bit_value = (sample_num >> line_number) & 1 + data.append(bit_value) + return data + + +def _get_expected_digital_data(num_lines: int, num_samples: int) -> list[int]: + return [ + _get_expected_digital_data_for_sample(num_lines, sample_number) + for sample_number in range(num_samples) + ] + + +def _get_expected_digital_port_data_port_major( + task: nidaqmx.Task, num_samples: int +) -> list[list[int]]: + return [_get_expected_digital_data(chan.di_num_lines, num_samples) for chan in task.channels] + + +def _get_expected_digital_port_data_sample_major( + task: nidaqmx.Task, num_samples: int +) -> list[list[int]]: + result = _get_expected_digital_port_data_port_major(task, num_samples) + return numpy.transpose(result).tolist() + + +def _bool_array_to_int(bool_array: numpy.typing.NDArray[numpy.bool_]) -> int: + result = 0 + # Simulated data is little-endian + for bit in bool_array[::-1]: + result = (result << 1) | int(bit) + return result + + +def _get_waveform_data(waveform: DigitalWaveform) -> list[int]: + assert isinstance(waveform, DigitalWaveform) + return [_bool_array_to_int(sample) for sample in waveform.data] + + +def _read_and_copy( + read_func: Callable[[numpy.typing.NDArray[_D]], None], array: numpy.typing.NDArray[_D] +) -> numpy.typing.NDArray[_D]: + read_func(array) + return array.copy() + + +def _is_timestamp_close_to_now(timestamp: ht_datetime, tolerance_seconds: float = 1.0) -> bool: + current_time = ht_datetime.now(timezone.utc) + time_diff = abs((timestamp - current_time).total_seconds()) + return time_diff <= tolerance_seconds + + +def _assert_equal_2d(data: list[list[float]], expected: list[list[float]], abs: float) -> None: + assert len(data) == len(expected) + for i in range(len(data)): + assert data[i] == pytest.approx(expected[i], abs=abs) + + +_D = TypeVar("_D", bound=numpy.generic) + +VOLTAGE_EPSILON = 1e-3 +VOLTAGE_CODE_EPSILON = round(_volts_to_codes(VOLTAGE_EPSILON)) +POWER_EPSILON = 1e-3 +POWER_BINARY_EPSILON = 1 + + +@pytest.fixture +def ai_single_channel_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a single-channel AI task.""" + offset = _get_voltage_offset_for_chan(0) + task.ai_channels.add_ai_voltage_chan( + sim_6363_device.ai_physical_chans[0].name, + min_val=offset, + max_val=offset + VOLTAGE_EPSILON, + ) + return task + + +@pytest.fixture +def ai_single_channel_task_with_timing( + ai_single_channel_task: nidaqmx.Task, +) -> nidaqmx.Task: + """Configure a single-channel AI task with timing.""" + ai_single_channel_task.timing.cfg_samp_clk_timing( + 1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 + ) + return ai_single_channel_task + + +@pytest.fixture +def ai_single_channel_task_with_high_rate( + task: nidaqmx.Task, sim_charge_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a single-channel AI task with high sampling rate.""" + offset = _get_voltage_offset_for_chan(0) + task.ai_channels.add_ai_voltage_chan( + sim_charge_device.ai_physical_chans[0].name, + min_val=offset, + max_val=offset + VOLTAGE_EPSILON, + ) + task.timing.cfg_samp_clk_timing( + rate=10_000_000, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 + ) + return task + + +@pytest.fixture +def ai_multi_channel_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a multi-channel AI task.""" + for chan_index in range(3): + offset = _get_voltage_offset_for_chan(chan_index) + chan = task.ai_channels.add_ai_voltage_chan( + sim_6363_device.ai_physical_chans[chan_index].name, + min_val=offset, + # min and max must be different, so add a small epsilon + max_val=offset + VOLTAGE_EPSILON, + ) + # forcing the maximum range for binary read scaling to be predictable + chan.ai_rng_high = 10 + chan.ai_rng_low = -10 + + return task + + +@pytest.fixture +def ai_multi_channel_task_with_timing( + ai_multi_channel_task: nidaqmx.Task, +) -> nidaqmx.Task: + """Configure a multi-channel AI task with timing.""" + ai_multi_channel_task.timing.cfg_samp_clk_timing( + 1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 + ) + return ai_multi_channel_task + + +@pytest.fixture +def pwr_single_channel_task( + task: nidaqmx.Task, sim_ts_power_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a single-channel power task.""" + task.ai_channels.add_ai_power_chan( + f"{sim_ts_power_device.name}/power", + _get_voltage_setpoint_for_chan(0), + _get_current_setpoint_for_chan(0), + True, # output enable + ) + return task + + +@pytest.fixture +def pwr_multi_channel_task( + task: nidaqmx.Task, sim_ts_power_devices: list[nidaqmx.system.Device] +) -> nidaqmx.Task: + """Configure a multi-channel power task.""" + for chan_index, sim_ts_power_device in enumerate(sim_ts_power_devices): + task.ai_channels.add_ai_power_chan( + f"{sim_ts_power_device.name}/power", + _get_voltage_setpoint_for_chan(chan_index), + _get_current_setpoint_for_chan(chan_index), + True, # output enable + ) + return task + + +@pytest.fixture +def di_single_line_task(task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device) -> nidaqmx.Task: + """Configure a single-line digital input task.""" + task.di_channels.add_di_chan( + sim_6363_device.di_lines[0].name, line_grouping=LineGrouping.CHAN_FOR_ALL_LINES + ) + return task + + +@pytest.fixture +def di_single_line_timing_task(di_single_line_task: nidaqmx.Task) -> nidaqmx.Task: + """Configure timing for a single-line digital input task.""" + di_single_line_task.timing.cfg_samp_clk_timing( + rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 + ) + return di_single_line_task + + +@pytest.fixture +def di_single_line_high_rate_task(di_single_line_task: nidaqmx.Task) -> nidaqmx.Task: + """Configure a high-rate single-line digital input task.""" + di_single_line_task.timing.cfg_samp_clk_timing( + rate=10_000_000, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 + ) + return di_single_line_task + + +@pytest.fixture +def di_single_channel_multi_line_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a single-channel multi-line digital input task.""" + task.di_channels.add_di_chan( + flatten_channel_string(sim_6363_device.di_lines.channel_names[:8]), + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + return task + + +@pytest.fixture +def di_single_channel_multi_line_timing_task( + di_single_channel_multi_line_task: nidaqmx.Task, +) -> nidaqmx.Task: + """Configure timing for a single-channel multi-line digital input task.""" + di_single_channel_multi_line_task.timing.cfg_samp_clk_timing( + rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 + ) + return di_single_channel_multi_line_task + + +@pytest.fixture +def di_single_channel_timing_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure timing for a single-channel digital input task.""" + task.di_channels.add_di_chan( + sim_6363_device.di_lines[0].name, line_grouping=LineGrouping.CHAN_FOR_ALL_LINES + ) + task.timing.cfg_samp_clk_timing(1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50) + return task + + +@pytest.fixture +def di_single_chan_lines_and_port_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a single-channel multi-line digital input task.""" + task.di_channels.add_di_chan( + flatten_channel_string( + sim_6363_device.di_lines.channel_names[0:3] + [sim_6363_device.di_ports[1].name] + ), + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + return task + + +@pytest.fixture +def di_multi_channel_multi_line_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a multi-channel digital input task.""" + task.di_channels.add_di_chan( + flatten_channel_string(sim_6363_device.di_lines.channel_names[:8]), + line_grouping=LineGrouping.CHAN_PER_LINE, + ) + return task + + +@pytest.fixture +def di_multi_chan_multi_line_timing_task( + di_multi_channel_multi_line_task: nidaqmx.Task, +) -> nidaqmx.Task: + """Configure timing for a multi-channel digital input task.""" + di_multi_channel_multi_line_task.timing.cfg_samp_clk_timing( + rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 + ) + return di_multi_channel_multi_line_task + + +@pytest.fixture +def di_multi_chan_diff_lines_timing_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a multi-channel digital input task. + + This task has three channels made up of different numbers of lines. + """ + task.di_channels.add_di_chan( + flatten_channel_string(sim_6363_device.di_lines.channel_names[0:1]), + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + task.di_channels.add_di_chan( + flatten_channel_string(sim_6363_device.di_lines.channel_names[1:3]), + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + task.di_channels.add_di_chan( + flatten_channel_string(sim_6363_device.di_lines.channel_names[3:7]), + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + task.timing.cfg_samp_clk_timing( + rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 + ) + return task + + +@pytest.fixture +def di_multi_chan_lines_and_port_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a multi-channel digital input task. + + This task has three channels made up of lines and one channel made up of a port. + """ + task.di_channels.add_di_chan( + flatten_channel_string(sim_6363_device.di_lines.channel_names[0:1]), + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + task.di_channels.add_di_chan( + flatten_channel_string(sim_6363_device.di_lines.channel_names[1:3]), + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + task.di_channels.add_di_chan( + flatten_channel_string(sim_6363_device.di_lines.channel_names[3:7]), + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + task.di_channels.add_di_chan( + sim_6363_device.di_ports[1].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + return task + + +@pytest.fixture +def di_single_channel_port_byte_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a single-channel digital input task with a channel made up of an 8-line port.""" + # 6363 port 1 has 8 lines + task.di_channels.add_di_chan( + sim_6363_device.di_ports[1].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + return task + + +@pytest.fixture +def di_multi_channel_port_byte_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a multi-channel digital input task with channels made up of 8-line ports.""" + # 6363 port 1 has 8 lines + task.di_channels.add_di_chan( + sim_6363_device.di_ports[1].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + # 6363 port 2 has 8 lines + task.di_channels.add_di_chan( + sim_6363_device.di_ports[2].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + return task + + +@pytest.fixture +def di_single_channel_port_uint16_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a single-channel digital input task with a channel made up of an 8-line port.""" + # 6363 port 1 has 8 lines, and DAQ will happily extend the data to the larger type. + task.di_channels.add_di_chan( + sim_6363_device.di_ports[1].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + return task + + +@pytest.fixture +def di_multi_channel_port_uint16_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a multi-channel digital input task with channels made up of 8-line ports.""" + # 6363 port 1 has 8 lines, and DAQ will happily extend the data to the larger type. + task.di_channels.add_di_chan( + sim_6363_device.di_ports[1].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + # 6363 port 2 has 8 lines, and DAQ will happily extend the data to the larger type. + task.di_channels.add_di_chan( + sim_6363_device.di_ports[2].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + return task + + +@pytest.fixture +def di_single_channel_port_uint32_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a single-channel digital input task with a channel made up of a 32-line port.""" + # 6363 port 0 has 32 lines + task.di_channels.add_di_chan( + sim_6363_device.di_ports[0].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + return task + + +@pytest.fixture +def di_single_channel_port_uint32_timing_task( + di_single_channel_port_uint32_task: nidaqmx.Task, +) -> nidaqmx.Task: + """Configure timing for a single-channel digital input task. + + This task has a channel made up of a 32-line port. + """ + di_single_channel_port_uint32_task.timing.cfg_samp_clk_timing( + rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 + ) + return di_single_channel_port_uint32_task + + +@pytest.fixture +def di_multi_channel_port_uint32_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure a multi-channel digital input task. + + This task has channels made up of a 32-line port and two 8-line ports. + """ + # 6363 port 0 has 32 lines + task.di_channels.add_di_chan( + sim_6363_device.di_ports[0].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + # 6363 port 1 has 8 lines, and DAQ will happily extend the data to the larger type. + task.di_channels.add_di_chan( + sim_6363_device.di_ports[1].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + # 6363 port 2 has 8 lines, and DAQ will happily extend the data to the larger type. + task.di_channels.add_di_chan( + sim_6363_device.di_ports[2].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + return task + + +@pytest.fixture +def di_multi_channel_timing_task( + task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device +) -> nidaqmx.Task: + """Configure timing for a multi-channel digital input task.""" + task.di_channels.add_di_chan( + flatten_channel_string(sim_6363_device.di_lines.channel_names[:8]), + line_grouping=LineGrouping.CHAN_PER_LINE, + ) + task.timing.cfg_samp_clk_timing(1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50) + return task diff --git a/tests/component/readers/test_analog_multi_channel_reader.py b/tests/component/readers/test_analog_multi_channel_reader.py new file mode 100644 index 000000000..bb35e63ce --- /dev/null +++ b/tests/component/readers/test_analog_multi_channel_reader.py @@ -0,0 +1,339 @@ +from __future__ import annotations + +import ctypes +import math + +import numpy +import pytest +from hightime import datetime as ht_datetime, timedelta as ht_timedelta +from nitypes.waveform import AnalogWaveform, SampleIntervalMode + +import nidaqmx +from nidaqmx._feature_toggles import WAVEFORM_SUPPORT, FeatureNotSupportedError +from nidaqmx.constants import WaveformAttributeMode +from nidaqmx.error_codes import DAQmxErrors +from nidaqmx.stream_readers import AnalogMultiChannelReader, DaqError +from .conftest import ( + VOLTAGE_EPSILON, + _get_voltage_offset_for_chan, + _is_timestamp_close_to_now, +) + + +def test___analog_multi_channel_reader___read_one_sample___returns_valid_samples( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + data = numpy.full(num_channels, math.inf, dtype=numpy.float64) + + reader.read_one_sample(data) + + expected = [_get_voltage_offset_for_chan(chan_index) for chan_index in range(num_channels)] + assert data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + + +def test___analog_multi_channel_reader___read_one_sample_with_wrong_dtype___raises_error_with_correct_dtype( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + data = numpy.full(num_channels, math.inf, dtype=numpy.float32) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + reader.read_one_sample(data) + + assert "float64" in exc_info.value.args[0] + + +def test___analog_multi_channel_reader___read_many_sample___returns_valid_samples( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) + + samples_read = reader.read_many_sample(data, samples_to_read) + + assert samples_read == samples_to_read + expected_vals = [_get_voltage_offset_for_chan(chan_index) for chan_index in range(num_channels)] + assert data == pytest.approx(expected_vals, abs=VOLTAGE_EPSILON) + + +def test___analog_multi_channel_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float32) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample(data, samples_to_read) + + assert "float64" in exc_info.value.args[0] + + +@pytest.mark.disable_feature_toggle(WAVEFORM_SUPPORT) +def test___analog_multi_channel_reader___read_waveforms_feature_disabled___raises_feature_not_supported_error( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) + num_channels = ai_multi_channel_task_with_timing.number_of_channels + samples_to_read = 10 + waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] + + with pytest.raises(FeatureNotSupportedError) as exc_info: + reader.read_waveforms(waveforms) + + error_message = str(exc_info.value) + assert "WAVEFORM_SUPPORT feature is not supported" in error_message + assert "NIDAQMX_ENABLE_WAVEFORM_SUPPORT" in error_message + + +@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") +def test___analog_multi_channel_reader___read_waveforms___returns_valid_waveforms( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) + num_channels = ai_multi_channel_task_with_timing.number_of_channels + samples_to_read = 10 + waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 3 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan_index, waveform in enumerate(waveforms): + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(chan_index) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert ( + waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name + ) + assert waveform.units == "Volts" + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") +def test___analog_multi_channel_reader___read_waveforms_no_args___returns_valid_waveforms( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) + num_channels = ai_multi_channel_task_with_timing.number_of_channels + waveforms = [AnalogWaveform(50) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms) + + assert samples_read == 50 + assert num_channels == 3 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan_index, waveform in enumerate(waveforms): + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(chan_index) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert ( + waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name + ) + assert waveform.units == "Volts" + assert waveform.sample_count == 50 + + +@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") +def test___analog_multi_channel_reader___read_waveforms_in_place___populates_valid_waveforms( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) + num_channels = ai_multi_channel_task_with_timing.number_of_channels + samples_to_read = 10 + + waveforms = [ + AnalogWaveform(samples_to_read), + AnalogWaveform(samples_to_read), + AnalogWaveform(samples_to_read), + ] + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 3 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan_index, waveform in enumerate(waveforms): + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(chan_index) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert ( + waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name + ) + assert waveform.units == "Volts" + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") +def test___analog_multi_channel_reader___read_into_undersized_waveforms___throws_exception( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) + samples_to_read = 10 + + waveforms = [ + AnalogWaveform(samples_to_read), + AnalogWaveform(samples_to_read - 1), + AnalogWaveform(samples_to_read), + ] + with pytest.raises(DaqError) as exc_info: + reader.read_waveforms(waveforms, samples_to_read) + + assert exc_info.value.error_code == DAQmxErrors.READ_BUFFER_TOO_SMALL + assert exc_info.value.args[0].startswith("The waveform at index 1 does not have enough space") + + +@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") +def test___analog_multi_channel_reader___read_with_wrong_number_of_waveforms___throws_exception( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) + samples_to_read = 10 + + waveforms = [ + AnalogWaveform(samples_to_read), + AnalogWaveform(samples_to_read), + ] + with pytest.raises(DaqError) as exc_info: + reader.read_waveforms(waveforms, samples_to_read) + + assert exc_info.value.error_code == DAQmxErrors.MISMATCHED_INPUT_ARRAY_SIZES + assert "does not match the number of channels" in exc_info.value.args[0] + + +@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") +def test___analog_multi_channel_reader_with_timing_flag___read_waveforms___only_includes_timing_data( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + in_stream = ai_multi_channel_task_with_timing.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.TIMING + reader = AnalogMultiChannelReader(in_stream) + num_channels = ai_multi_channel_task_with_timing.number_of_channels + samples_to_read = 10 + waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 3 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan_index, waveform in enumerate(waveforms): + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(chan_index) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == "" + assert waveform.units == "" + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") +def test___analog_multi_channel_reader_with_extended_properties_flag___read_waveforms___only_includes_extended_properties( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + in_stream = ai_multi_channel_task_with_timing.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.EXTENDED_PROPERTIES + reader = AnalogMultiChannelReader(in_stream) + num_channels = ai_multi_channel_task_with_timing.number_of_channels + samples_to_read = 10 + waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 3 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan_index, waveform in enumerate(waveforms): + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(chan_index) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE + assert ( + waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name + ) + assert waveform.units == "Volts" + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") +def test___analog_multi_channel_reader_with_both_flags___read_waveforms___includes_both_timing_and_extended_properties( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + in_stream = ai_multi_channel_task_with_timing.in_stream + in_stream.waveform_attribute_mode = ( + WaveformAttributeMode.TIMING | WaveformAttributeMode.EXTENDED_PROPERTIES + ) + reader = AnalogMultiChannelReader(in_stream) + num_channels = ai_multi_channel_task_with_timing.number_of_channels + samples_to_read = 10 + waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 3 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan_index, waveform in enumerate(waveforms): + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(chan_index) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert ( + waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name + ) + assert waveform.units == "Volts" + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") +def test___analog_multi_channel_reader_with_none_flag___read_waveforms___minimal_waveform_data( + ai_multi_channel_task_with_timing: nidaqmx.Task, +) -> None: + in_stream = ai_multi_channel_task_with_timing.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.NONE + reader = AnalogMultiChannelReader(in_stream) + num_channels = ai_multi_channel_task_with_timing.number_of_channels + samples_to_read = 10 + waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 3 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan_index, waveform in enumerate(waveforms): + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(chan_index) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE + assert waveform.channel_name == "" + assert waveform.units == "" + assert waveform.sample_count == samples_to_read diff --git a/tests/component/readers/test_analog_single_channel_reader.py b/tests/component/readers/test_analog_single_channel_reader.py new file mode 100644 index 000000000..e611a5e6a --- /dev/null +++ b/tests/component/readers/test_analog_single_channel_reader.py @@ -0,0 +1,306 @@ +from __future__ import annotations + +import ctypes +import math +from typing import Callable + +import numpy +import pytest +from hightime import datetime as ht_datetime, timedelta as ht_timedelta +from nitypes.waveform import AnalogWaveform, SampleIntervalMode + +import nidaqmx +import nidaqmx.system +from nidaqmx._feature_toggles import WAVEFORM_SUPPORT, FeatureNotSupportedError +from nidaqmx.constants import AcquisitionType, WaveformAttributeMode +from nidaqmx.error_codes import DAQmxErrors +from nidaqmx.stream_readers import AnalogSingleChannelReader, DaqError +from .conftest import ( + VOLTAGE_EPSILON, + _get_voltage_offset_for_chan, + _is_timestamp_close_to_now, +) + + +def test___analog_single_channel_reader___read_one_sample___returns_valid_samples( + ai_single_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogSingleChannelReader(ai_single_channel_task.in_stream) + + data = reader.read_one_sample() + + expected = _get_voltage_offset_for_chan(0) + assert data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + + +def test___analog_single_channel_reader___read_many_sample___returns_valid_samples( + ai_single_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogSingleChannelReader(ai_single_channel_task.in_stream) + samples_to_read = 10 + data = numpy.full(samples_to_read, math.inf, dtype=numpy.float64) + + samples_read = reader.read_many_sample(data, samples_to_read) + + assert samples_read == samples_to_read + expected = _get_voltage_offset_for_chan(0) + assert data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + + +def test___analog_single_channel_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( + ai_single_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogSingleChannelReader(ai_single_channel_task.in_stream) + samples_to_read = 10 + data = numpy.full(samples_to_read, math.inf, dtype=numpy.float32) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample(data, samples_to_read) + + assert "float64" in exc_info.value.args[0] + + +@pytest.mark.disable_feature_toggle(WAVEFORM_SUPPORT) +def test___analog_single_channel_reader___read_waveform_feature_disabled___raises_feature_not_supported_error( + ai_single_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) + waveform = AnalogWaveform(50) + + with pytest.raises(FeatureNotSupportedError) as exc_info: + reader.read_waveform(waveform) + + error_message = str(exc_info.value) + assert "WAVEFORM_SUPPORT feature is not supported" in error_message + assert "NIDAQMX_ENABLE_WAVEFORM_SUPPORT" in error_message + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader___read_waveform___returns_valid_waveform( + ai_single_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) + samples_to_read = 10 + waveform = AnalogWaveform(samples_to_read) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(0) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name + assert waveform.units == "Volts" + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader___read_waveform_no_args___returns_valid_waveform( + ai_single_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) + waveform = AnalogWaveform(50) + + samples_read = reader.read_waveform(waveform) + + assert samples_read == 50 + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(0) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name + assert waveform.units == "Volts" + assert waveform.sample_count == 50 + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader___read_waveform_in_place___populates_valid_waveform( + ai_single_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) + samples_to_read = 10 + + waveform = AnalogWaveform(samples_to_read) + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(0) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name + assert waveform.units == "Volts" + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader___reuse_waveform_in_place___overwrites_data_timing_and_attributes( + generate_task: Callable[[], nidaqmx.Task], sim_6363_device: nidaqmx.system.Device +) -> None: + def _make_single_channel_reader(chan_index, offset, rate): + task = generate_task() + task.ai_channels.add_ai_voltage_chan( + sim_6363_device.ai_physical_chans[chan_index].name, + min_val=offset, + max_val=offset + VOLTAGE_EPSILON, + ) + task.timing.cfg_samp_clk_timing(rate, sample_mode=AcquisitionType.FINITE, samps_per_chan=10) + return AnalogSingleChannelReader(task.in_stream) + + reader0 = _make_single_channel_reader(chan_index=0, offset=0, rate=1000.0) + reader1 = _make_single_channel_reader(chan_index=1, offset=1, rate=2000.0) + waveform = AnalogWaveform(10) + + reader0.read_waveform(waveform, 10) + timestamp1 = waveform.timing.timestamp + assert waveform.scaled_data == pytest.approx(0, abs=VOLTAGE_EPSILON) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == f"{sim_6363_device.name}/ai0" + + reader1.read_waveform(waveform, 10) + timestamp2 = waveform.timing.timestamp + assert waveform.scaled_data == pytest.approx(1, abs=VOLTAGE_EPSILON) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 2000) + assert waveform.channel_name == f"{sim_6363_device.name}/ai1" + + assert timestamp2 > timestamp1 + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader___read_into_undersized_waveform___throws_exception( + ai_single_channel_task_with_timing: nidaqmx.Task, +) -> None: + reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) + samples_to_read = 10 + + waveform = AnalogWaveform(samples_to_read - 1) + with pytest.raises(DaqError) as exc_info: + reader.read_waveform(waveform, samples_to_read) + + assert exc_info.value.error_code == DAQmxErrors.READ_BUFFER_TOO_SMALL + assert exc_info.value.args[0].startswith("The provided waveform does not have enough space") + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader___read_waveform_high_sample_rate___returns_correct_sample_interval( + ai_single_channel_task_with_high_rate: nidaqmx.Task, +) -> None: + reader = AnalogSingleChannelReader(ai_single_channel_task_with_high_rate.in_stream) + samples_to_read = 50 + waveform = AnalogWaveform(samples_to_read) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert isinstance(waveform, AnalogWaveform) + expected = _get_voltage_offset_for_chan(0) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 10_000_000) + assert waveform.channel_name == ai_single_channel_task_with_high_rate.ai_channels[0].name + assert waveform.units == "Volts" + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader_with_timing_flag___read_waveform___only_includes_timing_data( + ai_single_channel_task_with_timing: nidaqmx.Task, +) -> None: + in_stream = ai_single_channel_task_with_timing.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.TIMING + reader = AnalogSingleChannelReader(in_stream) + samples_to_read = 10 + waveform = AnalogWaveform(samples_to_read) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert isinstance(waveform, AnalogWaveform) + assert waveform.sample_count == samples_to_read + expected = _get_voltage_offset_for_chan(0) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == "" + assert waveform.units == "" + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader_with_extended_properties_flag___read_waveform___only_includes_extended_properties( + ai_single_channel_task_with_timing: nidaqmx.Task, +) -> None: + in_stream = ai_single_channel_task_with_timing.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.EXTENDED_PROPERTIES + reader = AnalogSingleChannelReader(in_stream) + samples_to_read = 10 + waveform = AnalogWaveform(samples_to_read) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert isinstance(waveform, AnalogWaveform) + assert waveform.sample_count == samples_to_read + expected = _get_voltage_offset_for_chan(0) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE + assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name + assert waveform.units == "Volts" + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader_with_both_flags___read_waveform___includes_both_timing_and_extended_properties( + ai_single_channel_task_with_timing: nidaqmx.Task, +) -> None: + in_stream = ai_single_channel_task_with_timing.in_stream + in_stream.waveform_attribute_mode = ( + WaveformAttributeMode.TIMING | WaveformAttributeMode.EXTENDED_PROPERTIES + ) + reader = AnalogSingleChannelReader(in_stream) + samples_to_read = 10 + waveform = AnalogWaveform(samples_to_read) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert isinstance(waveform, AnalogWaveform) + assert waveform.sample_count == samples_to_read + expected = _get_voltage_offset_for_chan(0) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert isinstance(waveform.timing.timestamp, ht_datetime) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name + assert waveform.units == "Volts" + + +@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") +def test___analog_single_channel_reader_with_none_flag___read_waveform___minimal_waveform_data( + ai_single_channel_task_with_timing: nidaqmx.Task, +) -> None: + in_stream = ai_single_channel_task_with_timing.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.NONE + reader = AnalogSingleChannelReader(in_stream) + samples_to_read = 10 + waveform = AnalogWaveform(samples_to_read) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert isinstance(waveform, AnalogWaveform) + assert waveform.sample_count == samples_to_read + expected = _get_voltage_offset_for_chan(0) + assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE + assert waveform.channel_name == "" + assert waveform.units == "" diff --git a/tests/component/readers/test_analog_unscaled_reader.py b/tests/component/readers/test_analog_unscaled_reader.py new file mode 100644 index 000000000..0aa7a5460 --- /dev/null +++ b/tests/component/readers/test_analog_unscaled_reader.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +import ctypes +import math + +import numpy +import pytest + +import nidaqmx +from nidaqmx.stream_readers import AnalogUnscaledReader +from .conftest import ( + VOLTAGE_CODE_EPSILON, + _get_voltage_code_offset_for_chan, +) + + +def test___analog_unscaled_reader___read_int16___returns_valid_samples( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.int16).min, dtype=numpy.int16 + ) + + samples_read = reader.read_int16(data, number_of_samples_per_channel=samples_to_read) + + assert samples_read == samples_to_read + expected_vals = [ + _get_voltage_code_offset_for_chan(chan_index) for chan_index in range(num_channels) + ] + assert data == pytest.approx(expected_vals, abs=VOLTAGE_CODE_EPSILON) + + +def test___analog_unscaled_reader___read_int16___raises_error_with_correct_dtype( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_int16(data, number_of_samples_per_channel=samples_to_read) + + assert "int16" in exc_info.value.args[0] + + +def test___analog_unscaled_reader___read_uint16___returns_valid_samples( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16 + ) + + samples_read = reader.read_uint16(data, number_of_samples_per_channel=samples_to_read) + + assert samples_read == samples_to_read + expected_vals = [ + _get_voltage_code_offset_for_chan(chan_index) for chan_index in range(num_channels) + ] + # Promote to larger signed type to avoid overflow w/NumPy 2.0+. + assert data.astype(numpy.int32) == pytest.approx(expected_vals, abs=VOLTAGE_CODE_EPSILON) + + +def test___analog_unscaled_reader___read_uint16___raises_error_with_correct_dtype( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_uint16(data, number_of_samples_per_channel=samples_to_read) + + assert "uint16" in exc_info.value.args[0] + + +def test___analog_unscaled_reader___read_int32___returns_valid_samples( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.int32).min, dtype=numpy.int32 + ) + + samples_read = reader.read_int32(data, number_of_samples_per_channel=samples_to_read) + + assert samples_read == samples_to_read + expected_vals = [ + _get_voltage_code_offset_for_chan(chan_index) for chan_index in range(num_channels) + ] + assert data == pytest.approx(expected_vals, abs=VOLTAGE_CODE_EPSILON) + + +def test___analog_unscaled_reader___read_int32___raises_error_with_correct_dtype( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_int32(data, number_of_samples_per_channel=samples_to_read) + + assert "int32" in exc_info.value.args[0] + + +def test___analog_unscaled_reader___read_uint32___returns_valid_samples( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32 + ) + + samples_read = reader.read_uint32(data, number_of_samples_per_channel=samples_to_read) + + assert samples_read == samples_to_read + expected_vals = [ + _get_voltage_code_offset_for_chan(chan_index) for chan_index in range(num_channels) + ] + # Promote to larger signed type to avoid overflow w/NumPy 2.0+. + assert data.astype(numpy.int64) == pytest.approx(expected_vals, abs=VOLTAGE_CODE_EPSILON) + + +def test___analog_unscaled_reader___read_uint32___raises_error_with_correct_dtype( + ai_multi_channel_task: nidaqmx.Task, +) -> None: + reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) + num_channels = ai_multi_channel_task.number_of_channels + samples_to_read = 10 + data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_uint32(data, number_of_samples_per_channel=samples_to_read) + + assert "uint32" in exc_info.value.args[0] diff --git a/tests/component/readers/test_digital_multi_channel_reader.py b/tests/component/readers/test_digital_multi_channel_reader.py new file mode 100644 index 000000000..22c0d5fb8 --- /dev/null +++ b/tests/component/readers/test_digital_multi_channel_reader.py @@ -0,0 +1,716 @@ +from __future__ import annotations + +import ctypes +import math +from typing import Callable + +import numpy +import pytest +from hightime import timedelta as ht_timedelta +from nitypes.waveform import DigitalWaveform, SampleIntervalMode + +import nidaqmx +import nidaqmx.system +from nidaqmx._feature_toggles import WAVEFORM_SUPPORT, FeatureNotSupportedError +from nidaqmx.constants import AcquisitionType, LineGrouping, WaveformAttributeMode +from nidaqmx.error_codes import DAQmxErrors +from nidaqmx.stream_readers import DaqError, DigitalMultiChannelReader +from nidaqmx.utils import flatten_channel_string +from .conftest import ( + _bool_array_to_int, + _get_expected_data_for_line, + _get_expected_digital_data, + _get_expected_digital_port_data_port_major, + _get_expected_digital_port_data_sample_major, + _get_num_lines_in_task, + _get_waveform_data, + _is_timestamp_close_to_now, + _read_and_copy, +) + + +def test___digital_multi_channel_reader___read_one_sample_one_line___returns_valid_samples( + di_single_line_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_single_line_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_line_task) + samples_to_read = 256 + sample = numpy.full(num_lines, False, dtype=numpy.bool_) + + data = [_read_and_copy(reader.read_one_sample_one_line, sample) for _ in range(samples_to_read)] + + assert [_bool_array_to_int(sample) for sample in data] == _get_expected_digital_data( + num_lines, samples_to_read + ) + + +def test___digital_multi_channel_reader___read_one_sample_one_line_with_wrong_dtype___raises_error_with_correct_dtype( + di_single_line_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_single_line_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_line_task) + data = numpy.full(num_lines, math.inf, dtype=numpy.float64) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + reader.read_one_sample_one_line(data) + + assert "bool" in exc_info.value.args[0] + + +def test___digital_multi_channel_reader___read_one_sample_multi_line___returns_valid_samples( + di_multi_channel_multi_line_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_multi_line_task.in_stream) + num_channels = di_multi_channel_multi_line_task.number_of_channels + samples_to_read = 256 + sample = numpy.full((num_channels, 1), False, dtype=numpy.bool_) + + data = [ + _read_and_copy(reader.read_one_sample_multi_line, sample) for _ in range(samples_to_read) + ] + + assert [_bool_array_to_int(sample[:, 0]) for sample in data] == _get_expected_digital_data( + num_channels, samples_to_read + ) + + +def test___digital_multi_channel_reader___read_one_sample_multi_line_jagged___returns_valid_samples( + di_multi_channel_port_uint32_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) + num_channels = di_multi_channel_port_uint32_task.number_of_channels + samples_to_read = 256 + sample = numpy.full((num_channels, 32), False, dtype=numpy.bool_) + + data = [ + _read_and_copy(reader.read_one_sample_multi_line, sample) for _ in range(samples_to_read) + ] + + assert [ + [_bool_array_to_int(sample[chan, :]) for chan in range(num_channels)] for sample in data + ] == _get_expected_digital_port_data_sample_major( + di_multi_channel_port_uint32_task, samples_to_read + ) + + +def test___digital_multi_channel_reader___read_one_sample_multi_line_with_wrong_dtype___raises_error_with_correct_dtype( + di_multi_channel_multi_line_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_multi_line_task.in_stream) + num_channels = di_multi_channel_multi_line_task.number_of_channels + data = numpy.full((num_channels, 1), math.inf, dtype=numpy.float64) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + reader.read_one_sample_multi_line(data) + + assert "bool" in exc_info.value.args[0] + + +def test___digital_multi_channel_reader___read_one_sample_port_byte___returns_valid_samples( + di_multi_channel_port_byte_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_byte_task.in_stream) + num_channels = di_multi_channel_port_byte_task.number_of_channels + samples_to_read = 256 + sample = numpy.full(num_channels, numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8) + + data = [ + _read_and_copy(reader.read_one_sample_port_byte, sample).tolist() + for _ in range(samples_to_read) + ] + + assert data == _get_expected_digital_port_data_sample_major( + di_multi_channel_port_byte_task, samples_to_read + ) + + +def test___digital_multi_channel_reader___read_one_sample_port_byte_with_wrong_dtype___raises_error_with_correct_dtype( + di_multi_channel_port_byte_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_byte_task.in_stream) + num_channels = di_multi_channel_port_byte_task.number_of_channels + data = numpy.full(num_channels, numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + reader.read_one_sample_port_byte(data) + + assert "uint8" in exc_info.value.args[0] + + +def test___digital_multi_channel_reader___read_many_sample_port_byte___returns_valid_samples( + di_multi_channel_port_byte_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_byte_task.in_stream) + num_channels = di_multi_channel_port_byte_task.number_of_channels + samples_to_read = 256 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8 + ) + + samples_read = reader.read_many_sample_port_byte( + data, number_of_samples_per_channel=samples_to_read + ) + + assert samples_read == samples_to_read + assert data.tolist() == _get_expected_digital_port_data_port_major( + di_multi_channel_port_byte_task, samples_to_read + ) + + +def test___digital_multi_channel_reader___read_many_sample_port_byte_with_wrong_dtype___raises_error_with_correct_dtype( + di_multi_channel_port_byte_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_byte_task.in_stream) + num_channels = di_multi_channel_port_byte_task.number_of_channels + samples_to_read = 256 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16 + ) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample_port_byte(data, number_of_samples_per_channel=samples_to_read) + + assert "uint8" in exc_info.value.args[0] + + +def test___digital_multi_channel_reader___read_one_sample_port_uint16___returns_valid_samples( + di_multi_channel_port_uint16_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_uint16_task.in_stream) + num_channels = di_multi_channel_port_uint16_task.number_of_channels + samples_to_read = 256 + sample = numpy.full(num_channels, numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16) + + data = [ + _read_and_copy(reader.read_one_sample_port_uint16, sample).tolist() + for _ in range(samples_to_read) + ] + + assert data == _get_expected_digital_port_data_sample_major( + di_multi_channel_port_uint16_task, samples_to_read + ) + + +def test___digital_multi_channel_reader___read_one_sample_port_uint16_with_wrong_dtype___raises_error_with_correct_dtype( + di_multi_channel_port_uint16_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_uint16_task.in_stream) + num_channels = di_multi_channel_port_uint16_task.number_of_channels + data = numpy.full(num_channels, numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + reader.read_one_sample_port_uint16(data) + + assert "uint16" in exc_info.value.args[0] + + +def test___digital_multi_channel_reader___read_many_sample_port_uint16___returns_valid_samples( + di_multi_channel_port_uint16_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_uint16_task.in_stream) + num_channels = di_multi_channel_port_uint16_task.number_of_channels + samples_to_read = 256 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16 + ) + + samples_read = reader.read_many_sample_port_uint16( + data, number_of_samples_per_channel=samples_to_read + ) + + assert samples_read == samples_to_read + assert data.tolist() == _get_expected_digital_port_data_port_major( + di_multi_channel_port_uint16_task, samples_to_read + ) + + +def test___digital_multi_channel_reader___read_many_sample_port_uint16_with_wrong_dtype___raises_error_with_correct_dtype( + di_multi_channel_port_uint16_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_uint16_task.in_stream) + num_channels = di_multi_channel_port_uint16_task.number_of_channels + samples_to_read = 256 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32 + ) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample_port_uint16(data, number_of_samples_per_channel=samples_to_read) + + assert "uint16" in exc_info.value.args[0] + + +def test___digital_multi_channel_reader___read_one_sample_port_uint32___returns_valid_samples( + di_multi_channel_port_uint32_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) + num_channels = di_multi_channel_port_uint32_task.number_of_channels + samples_to_read = 256 + sample = numpy.full(num_channels, numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32) + + data = [ + _read_and_copy(reader.read_one_sample_port_uint32, sample).tolist() + for _ in range(samples_to_read) + ] + + assert data == _get_expected_digital_port_data_sample_major( + di_multi_channel_port_uint32_task, samples_to_read + ) + + +def test___digital_multi_channel_reader___read_one_sample_port_uint32_with_wrong_dtype___raises_error_with_correct_dtype( + di_multi_channel_port_uint32_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) + num_channels = di_multi_channel_port_uint32_task.number_of_channels + data = numpy.full(num_channels, numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + reader.read_one_sample_port_uint32(data) + + assert "uint32" in exc_info.value.args[0] + + +def test___digital_multi_channel_reader___read_many_sample_port_uint32___returns_valid_samples( + di_multi_channel_port_uint32_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) + num_channels = di_multi_channel_port_uint32_task.number_of_channels + samples_to_read = 256 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32 + ) + + samples_read = reader.read_many_sample_port_uint32( + data, number_of_samples_per_channel=samples_to_read + ) + + assert samples_read == samples_to_read + assert data.tolist() == _get_expected_digital_port_data_port_major( + di_multi_channel_port_uint32_task, samples_to_read + ) + + +def test___digital_multi_channel_reader___read_many_sample_port_uint32_with_wrong_dtype___raises_error_with_correct_dtype( + di_multi_channel_port_uint32_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) + num_channels = di_multi_channel_port_uint32_task.number_of_channels + samples_to_read = 256 + data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8 + ) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample_port_uint32(data, number_of_samples_per_channel=samples_to_read) + + assert "uint32" in exc_info.value.args[0] + + +@pytest.mark.disable_feature_toggle(WAVEFORM_SUPPORT) +def test___digital_multi_channel_multi_line_reader___read_waveforms_feature_disabled___raises_feature_not_supported_error( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) + waveforms = [DigitalWaveform(50) for _ in range(8)] + + with pytest.raises(FeatureNotSupportedError) as exc_info: + reader.read_waveforms(waveforms) + + error_message = str(exc_info.value) + assert "WAVEFORM_SUPPORT feature is not supported" in error_message + assert "NIDAQMX_ENABLE_WAVEFORM_SUPPORT" in error_message + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader___read_waveforms___returns_valid_waveforms( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) + num_channels = di_multi_chan_multi_line_timing_task.number_of_channels + num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) + samples_to_read = 10 + waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 8 + assert num_lines == 8 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan, waveform in enumerate(waveforms): + assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_different_lines_reader___read_waveforms___returns_valid_waveforms( + di_multi_chan_diff_lines_timing_task: nidaqmx.Task, + sim_6363_device: nidaqmx.system.Device, +) -> None: + reader = DigitalMultiChannelReader(di_multi_chan_diff_lines_timing_task.in_stream) + num_channels = di_multi_chan_diff_lines_timing_task.number_of_channels + num_lines = _get_num_lines_in_task(di_multi_chan_diff_lines_timing_task) + samples_to_read = 10 + waveforms = [ + DigitalWaveform(samples_to_read, 1), + DigitalWaveform(samples_to_read, 2), + DigitalWaveform(samples_to_read, 4), + ] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 3 + assert num_lines == 7 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + assert _get_waveform_data(waveforms[0]) == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] + assert _is_timestamp_close_to_now(waveforms[0].timing.timestamp) + assert waveforms[0].sample_count == samples_to_read + assert waveforms[0].timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveforms[0].channel_name == di_multi_chan_diff_lines_timing_task.di_channels[0].name + assert waveforms[0]._get_signal_names() == [ + sim_6363_device.di_lines[0].name, + ] + assert _get_waveform_data(waveforms[1]) == [0, 0, 1, 1, 2, 2, 3, 3, 0, 0] + assert _is_timestamp_close_to_now(waveforms[1].timing.timestamp) + assert waveforms[1].sample_count == samples_to_read + assert waveforms[1].timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveforms[1].channel_name == di_multi_chan_diff_lines_timing_task.di_channels[1].name + assert waveforms[1]._get_signal_names() == [ + sim_6363_device.di_lines[1].name, + sim_6363_device.di_lines[2].name, + ] + assert _get_waveform_data(waveforms[2]) == [0, 0, 0, 0, 0, 0, 0, 0, 1, 1] + assert _is_timestamp_close_to_now(waveforms[2].timing.timestamp) + assert waveforms[2].sample_count == samples_to_read + assert waveforms[2].timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveforms[2].channel_name == di_multi_chan_diff_lines_timing_task.di_channels[2].name + assert waveforms[2]._get_signal_names() == [ + sim_6363_device.di_lines[3].name, + sim_6363_device.di_lines[4].name, + sim_6363_device.di_lines[5].name, + sim_6363_device.di_lines[6].name, + ] + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_lines_and_port_reader___read_waveforms___returns_valid_waveforms( + di_multi_chan_lines_and_port_task: nidaqmx.Task, + sim_6363_device: nidaqmx.system.Device, +) -> None: + reader = DigitalMultiChannelReader(di_multi_chan_lines_and_port_task.in_stream) + num_channels = di_multi_chan_lines_and_port_task.number_of_channels + num_lines = _get_num_lines_in_task(di_multi_chan_lines_and_port_task) + samples_to_read = 10 + waveforms = [ + DigitalWaveform(samples_to_read, 1), + DigitalWaveform(samples_to_read, 2), + DigitalWaveform(samples_to_read, 4), + DigitalWaveform(samples_to_read, 8), + ] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 4 + assert num_lines == 15 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + assert _get_waveform_data(waveforms[0]) == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] + assert _is_timestamp_close_to_now(waveforms[0].timing.timestamp) + assert waveforms[0].sample_count == samples_to_read + assert waveforms[0].channel_name == di_multi_chan_lines_and_port_task.di_channels[0].name + assert waveforms[0]._get_signal_names() == [ + sim_6363_device.di_lines[0].name, + ] + assert _get_waveform_data(waveforms[1]) == [0, 0, 1, 1, 2, 2, 3, 3, 0, 0] + assert _is_timestamp_close_to_now(waveforms[1].timing.timestamp) + assert waveforms[1].sample_count == samples_to_read + assert waveforms[1].channel_name == di_multi_chan_lines_and_port_task.di_channels[1].name + assert waveforms[1]._get_signal_names() == [ + sim_6363_device.di_lines[1].name, + sim_6363_device.di_lines[2].name, + ] + assert _get_waveform_data(waveforms[2]) == [0, 0, 0, 0, 0, 0, 0, 0, 1, 1] + assert _is_timestamp_close_to_now(waveforms[2].timing.timestamp) + assert waveforms[2].sample_count == samples_to_read + assert waveforms[2].channel_name == di_multi_chan_lines_and_port_task.di_channels[2].name + assert waveforms[2]._get_signal_names() == [ + sim_6363_device.di_lines[3].name, + sim_6363_device.di_lines[4].name, + sim_6363_device.di_lines[5].name, + sim_6363_device.di_lines[6].name, + ] + # Note, the data on the port's waveform is MSB instead of LSB because of bug AB#3178052 + # When that bug is fixed, these asserts should be updated + assert _get_waveform_data(waveforms[3]) == [0, 128, 64, 192, 32, 160, 96, 224, 16, 144] + assert _is_timestamp_close_to_now(waveforms[3].timing.timestamp) + assert waveforms[3].sample_count == samples_to_read + assert waveforms[3].channel_name == di_multi_chan_lines_and_port_task.di_channels[3].name + assert waveforms[3]._get_signal_names() == [ + sim_6363_device.di_lines[39].name, + sim_6363_device.di_lines[38].name, + sim_6363_device.di_lines[37].name, + sim_6363_device.di_lines[36].name, + sim_6363_device.di_lines[35].name, + sim_6363_device.di_lines[34].name, + sim_6363_device.di_lines[33].name, + sim_6363_device.di_lines[32].name, + ] + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_different_lines_reader___read_mismatched_waveforms___throws_exception( + di_multi_chan_diff_lines_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_chan_diff_lines_timing_task.in_stream) + samples_to_read = 10 + waveforms = [ + DigitalWaveform(samples_to_read, 1), + DigitalWaveform( + samples_to_read, 3 + ), # mismatch - actually only two signals for this channel + DigitalWaveform(samples_to_read, 4), + ] + + with pytest.raises(ValueError) as exc_info: + reader.read_waveforms(waveforms, samples_to_read) + + error_message = str(exc_info.value) + assert "waveforms[1].data has 3 signals, but expected 2" in error_message + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader___read_waveforms_no_args___returns_valid_waveforms( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) + num_channels = di_multi_chan_multi_line_timing_task.number_of_channels + num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) + waveforms = [DigitalWaveform(50) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms) + + assert samples_read == 50 + assert num_channels == 8 + assert num_lines == 8 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan, waveform in enumerate(waveforms): + assert _get_waveform_data(waveform) == _get_expected_data_for_line(50, chan) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name + assert waveform.sample_count == 50 + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader___read_waveforms_in_place___populates_valid_waveforms( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) + num_channels = di_multi_chan_multi_line_timing_task.number_of_channels + num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) + samples_to_read = 10 + + waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 8 + assert num_lines == 8 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan, waveform in enumerate(waveforms): + assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader___reuse_waveform_in_place___overwrites_data_timing_and_attributes( + generate_task: Callable[[], nidaqmx.Task], sim_6363_device: nidaqmx.system.Device +) -> None: + def _make_multi_channel_multi_line_reader(lines_start, rate): + task = generate_task() + task.di_channels.add_di_chan( + flatten_channel_string( + sim_6363_device.di_lines.channel_names[lines_start : lines_start + 4] + ), + line_grouping=LineGrouping.CHAN_PER_LINE, + ) + task.timing.cfg_samp_clk_timing(rate, sample_mode=AcquisitionType.FINITE, samps_per_chan=10) + return DigitalMultiChannelReader(task.in_stream) + + sample_count = 10 + num_channels = 4 + reader0 = _make_multi_channel_multi_line_reader(lines_start=0, rate=1000.0) + reader1 = _make_multi_channel_multi_line_reader(lines_start=1, rate=2000.0) + waveforms = [DigitalWaveform(sample_count) for _ in range(num_channels)] + + reader0.read_waveforms(waveforms, sample_count) + timestamps0 = [wf.timing.timestamp for wf in waveforms] + for chan, waveform in enumerate(waveforms): + assert _get_waveform_data(waveform) == _get_expected_data_for_line(sample_count, chan) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == sim_6363_device.di_lines[chan].name + + reader1.read_waveforms(waveforms, sample_count) + timestamps1 = [wf.timing.timestamp for wf in waveforms] + for chan, waveform in enumerate(waveforms): + assert _get_waveform_data(waveform) == _get_expected_data_for_line(sample_count, chan + 1) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 2000) + assert waveform.channel_name == sim_6363_device.di_lines[chan + 1].name + + for ts0, ts1 in zip(timestamps0, timestamps1): + assert ts1 > ts0 + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader___read_into_undersized_waveforms___throws_exception( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) + num_channels = di_multi_chan_multi_line_timing_task.number_of_channels + samples_to_read = 10 + + waveforms = [DigitalWaveform(samples_to_read - 1) for _ in range(num_channels)] + with pytest.raises(DaqError) as exc_info: + reader.read_waveforms(waveforms, samples_to_read) + + assert exc_info.value.error_code == DAQmxErrors.READ_BUFFER_TOO_SMALL + assert exc_info.value.args[0].startswith("The waveform at index 0 does not have enough space") + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader___read_with_wrong_number_of_waveforms___throws_exception( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) + num_channels = di_multi_chan_multi_line_timing_task.number_of_channels + samples_to_read = 10 + + waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels - 1)] + with pytest.raises(DaqError) as exc_info: + reader.read_waveforms(waveforms, samples_to_read) + + assert exc_info.value.error_code == DAQmxErrors.MISMATCHED_INPUT_ARRAY_SIZES + assert "does not match the number of channels" in exc_info.value.args[0] + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader_with_timing_flag___read_waveforms___only_includes_timing_data( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + in_stream = di_multi_chan_multi_line_timing_task.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.TIMING + reader = DigitalMultiChannelReader(in_stream) + num_channels = di_multi_chan_multi_line_timing_task.number_of_channels + num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) + samples_to_read = 10 + waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 8 + assert num_lines == 8 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan, waveform in enumerate(waveforms): + assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == "" + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader_with_extended_properties_flag___read_waveforms___only_includes_extended_properties( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + in_stream = di_multi_chan_multi_line_timing_task.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.EXTENDED_PROPERTIES + reader = DigitalMultiChannelReader(in_stream) + num_channels = di_multi_chan_multi_line_timing_task.number_of_channels + num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) + samples_to_read = 10 + waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 8 + assert num_lines == 8 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan, waveform in enumerate(waveforms): + assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE + assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader_with_both_flags___read_waveforms___includes_both_timing_and_extended_properties( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + in_stream = di_multi_chan_multi_line_timing_task.in_stream + in_stream.waveform_attribute_mode = ( + WaveformAttributeMode.TIMING | WaveformAttributeMode.EXTENDED_PROPERTIES + ) + reader = DigitalMultiChannelReader(in_stream) + num_channels = di_multi_chan_multi_line_timing_task.number_of_channels + num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) + samples_to_read = 10 + waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 8 + assert num_lines == 8 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan, waveform in enumerate(waveforms): + assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name + assert waveform.sample_count == samples_to_read + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_multi_channel_multi_line_reader_with_none_flag___read_waveforms___minimal_waveform_data( + di_multi_chan_multi_line_timing_task: nidaqmx.Task, +) -> None: + in_stream = di_multi_chan_multi_line_timing_task.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.NONE + reader = DigitalMultiChannelReader(in_stream) + num_channels = di_multi_chan_multi_line_timing_task.number_of_channels + num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) + samples_to_read = 10 + waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] + + samples_read = reader.read_waveforms(waveforms, samples_to_read) + + assert samples_read == samples_to_read + assert num_channels == 8 + assert num_lines == 8 + assert isinstance(waveforms, list) + assert len(waveforms) == num_channels + for chan, waveform in enumerate(waveforms): + assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE + assert waveform.channel_name == "" + assert waveform.sample_count == samples_to_read diff --git a/tests/component/readers/test_digital_single_channel_reader.py b/tests/component/readers/test_digital_single_channel_reader.py new file mode 100644 index 000000000..56c904330 --- /dev/null +++ b/tests/component/readers/test_digital_single_channel_reader.py @@ -0,0 +1,544 @@ +from __future__ import annotations + +import ctypes +import math +from typing import Callable + +import numpy +import pytest +from hightime import timedelta as ht_timedelta +from nitypes.waveform import DigitalWaveform, SampleIntervalMode + +import nidaqmx +import nidaqmx.system +from nidaqmx._feature_toggles import WAVEFORM_SUPPORT, FeatureNotSupportedError +from nidaqmx.constants import AcquisitionType, LineGrouping, WaveformAttributeMode +from nidaqmx.error_codes import DAQmxErrors +from nidaqmx.stream_readers import DaqError, DigitalSingleChannelReader +from nidaqmx.utils import flatten_channel_string +from .conftest import ( + _bool_array_to_int, + _get_expected_data_for_line, + _get_expected_digital_data, + _get_num_lines_in_task, + _get_waveform_data, + _is_timestamp_close_to_now, + _read_and_copy, +) + + +def test___digital_single_channel_reader___read_one_sample_one_line___returns_valid_samples( + di_single_line_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_line_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_line_task) + samples_to_read = 256 + + data = [reader.read_one_sample_one_line() for _ in range(samples_to_read)] + + assert data == _get_expected_digital_data(num_lines, samples_to_read) + + +def test___digital_single_channel_reader___read_one_sample_multi_line___returns_valid_samples( + di_single_channel_multi_line_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_multi_line_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_multi_line_task) + samples_to_read = 256 + sample = numpy.full(num_lines, False, dtype=numpy.bool_) + + data = [ + _read_and_copy(reader.read_one_sample_multi_line, sample) for _ in range(samples_to_read) + ] + + assert [_bool_array_to_int(sample) for sample in data] == _get_expected_digital_data( + num_lines, samples_to_read + ) + + +def test___digital_single_channel_reader___read_one_sample_multi_line_with_wrong_dtype___raises_error_with_correct_dtype( + di_single_channel_multi_line_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_multi_line_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_multi_line_task) + data = numpy.full(num_lines, math.inf, dtype=numpy.float64) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + reader.read_one_sample_multi_line(data) + + assert "bool" in exc_info.value.args[0] + + +def test___digital_single_channel_reader___read_one_sample_port_byte___returns_valid_samples( + di_single_channel_port_byte_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_byte_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_port_byte_task) + samples_to_read = 256 + + data = [reader.read_one_sample_port_byte() for _ in range(samples_to_read)] + + assert data == _get_expected_digital_data(num_lines, samples_to_read) + + +def test___digital_single_channel_reader___read_one_sample_port_uint16___returns_valid_samples( + di_single_channel_port_uint16_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_uint16_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_port_uint16_task) + samples_to_read = 256 + + data = [reader.read_one_sample_port_uint16() for _ in range(samples_to_read)] + + assert data == _get_expected_digital_data(num_lines, samples_to_read) + + +def test___digital_single_channel_reader___read_one_sample_port_uint32___returns_valid_samples( + di_single_channel_port_uint32_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_uint32_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_port_uint32_task) + samples_to_read = 256 + + data = [reader.read_one_sample_port_uint32() for _ in range(samples_to_read)] + + assert data == _get_expected_digital_data(num_lines, samples_to_read) + + +def test___digital_single_channel_reader___read_many_sample_port_byte___returns_valid_samples( + di_single_channel_port_byte_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_byte_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_port_byte_task) + samples_to_read = 256 + data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8) + + samples_read = reader.read_many_sample_port_byte( + data, number_of_samples_per_channel=samples_to_read + ) + + assert samples_read == samples_to_read + assert data.tolist() == _get_expected_digital_data(num_lines, samples_to_read) + + +def test___digital_single_channel_reader___read_many_sample_port_byte_with_wrong_dtype___raises_error_with_correct_dtype( + di_single_channel_port_byte_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_byte_task.in_stream) + samples_to_read = 256 + data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample_port_byte(data, number_of_samples_per_channel=samples_to_read) + + assert "uint8" in exc_info.value.args[0] + + +def test___digital_single_channel_reader___read_many_sample_port_uint16___returns_valid_samples( + di_single_channel_port_uint16_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_uint16_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_port_uint16_task) + samples_to_read = 256 + data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16) + + samples_read = reader.read_many_sample_port_uint16( + data, number_of_samples_per_channel=samples_to_read + ) + + assert samples_read == samples_to_read + assert data.tolist() == _get_expected_digital_data(num_lines, samples_to_read) + + +def test___digital_single_channel_reader___read_many_sample_port_uint16_with_wrong_dtype___raises_error_with_correct_dtype( + di_single_channel_port_uint16_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_uint16_task.in_stream) + samples_to_read = 256 + data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample_port_uint16(data, number_of_samples_per_channel=samples_to_read) + + assert "uint16" in exc_info.value.args[0] + + +def test___digital_single_channel_reader___read_many_sample_port_uint32___returns_valid_samples( + di_single_channel_port_uint32_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_uint32_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_port_uint32_task) + samples_to_read = 256 + data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32) + + samples_read = reader.read_many_sample_port_uint32( + data, number_of_samples_per_channel=samples_to_read + ) + + assert samples_read == samples_to_read + assert data.tolist() == _get_expected_digital_data(num_lines, samples_to_read) + + +def test___digital_single_channel_reader___read_many_sample_port_uint32_with_wrong_dtype___raises_error_with_correct_dtype( + di_single_channel_port_uint32_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_uint32_task.in_stream) + samples_to_read = 256 + data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample_port_uint32(data, number_of_samples_per_channel=samples_to_read) + + assert "uint32" in exc_info.value.args[0] + + +@pytest.mark.disable_feature_toggle(WAVEFORM_SUPPORT) +def test___digital_single_line_reader___read_waveform_feature_disabled___raises_feature_not_supported_error( + di_single_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) + waveform = DigitalWaveform(50) + + with pytest.raises(FeatureNotSupportedError) as exc_info: + reader.read_waveform(waveform) + + error_message = str(exc_info.value) + assert "WAVEFORM_SUPPORT feature is not supported" in error_message + assert "NIDAQMX_ENABLE_WAVEFORM_SUPPORT" in error_message + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader___read_waveform___returns_valid_waveform( + di_single_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) + samples_to_read = 10 + waveform = DigitalWaveform(samples_to_read) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert _get_waveform_data(waveform) == _get_expected_digital_data(1, samples_to_read) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_channel_multi_line_reader___read_waveform___returns_valid_waveform( + di_single_channel_multi_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_multi_line_timing_task.in_stream) + samples_to_read = 10 + num_lines = _get_num_lines_in_task(di_single_channel_multi_line_timing_task) + waveform = DigitalWaveform(samples_to_read, num_lines) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert _get_waveform_data(waveform) == _get_expected_digital_data(num_lines, samples_to_read) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.channel_name == di_single_channel_multi_line_timing_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader___read_waveform_no_args___returns_valid_waveform( + di_single_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) + waveform = DigitalWaveform(50) + + samples_read = reader.read_waveform(waveform) + + assert samples_read == 50 + assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_channel_multi_line_reader___read_waveform_no_args___returns_valid_waveform( + di_single_channel_multi_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_multi_line_timing_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_multi_line_timing_task) + waveform = DigitalWaveform(50, num_lines) + + samples_read = reader.read_waveform(waveform) + + assert samples_read == 50 + assert _get_waveform_data(waveform) == _get_expected_digital_data(num_lines, 50) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.channel_name == di_single_channel_multi_line_timing_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader___read_waveform_in_place___returns_valid_waveform( + di_single_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) + waveform = DigitalWaveform(50) + + samples_read = reader.read_waveform(waveform) + + assert samples_read == 50 + assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_channel_multi_line_reader___read_waveform_in_place___returns_valid_waveform( + di_single_channel_multi_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_multi_line_timing_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_multi_line_timing_task) + waveform = DigitalWaveform(sample_count=50, signal_count=8) + + samples_read = reader.read_waveform(waveform) + + assert samples_read == 50 + assert _get_waveform_data(waveform) == _get_expected_digital_data(num_lines, 50) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == di_single_channel_multi_line_timing_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader___reuse_waveform_in_place___overwrites_data_timing_and_attributes( + generate_task: Callable[[], nidaqmx.Task], sim_6363_device: nidaqmx.system.Device +) -> None: + def _make_single_line_reader(chan_index, rate): + task = generate_task() + task.di_channels.add_di_chan( + sim_6363_device.di_lines[chan_index].name, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + task.timing.cfg_samp_clk_timing(rate, sample_mode=AcquisitionType.FINITE, samps_per_chan=10) + return DigitalSingleChannelReader(task.in_stream) + + sample_count = 10 + reader0 = _make_single_line_reader(chan_index=0, rate=1000.0) + reader1 = _make_single_line_reader(chan_index=1, rate=2000.0) + waveform = DigitalWaveform(sample_count) + + reader0.read_waveform(waveform, sample_count) + timestamp0 = waveform.timing.timestamp + assert _get_waveform_data(waveform) == _get_expected_data_for_line(sample_count, 0) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == sim_6363_device.di_lines[0].name + + reader1.read_waveform(waveform, sample_count) + timestamp1 = waveform.timing.timestamp + assert _get_waveform_data(waveform) == _get_expected_data_for_line(sample_count, 1) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 2000) + assert waveform.channel_name == sim_6363_device.di_lines[1].name + + assert timestamp1 > timestamp0 + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_channel_multi_line_reader___reuse_waveform_in_place___overwrites_data_timing_and_attributes( + generate_task: Callable[[], nidaqmx.Task], sim_6363_device: nidaqmx.system.Device +) -> None: + def _make_single_channel_multi_line_reader(lines_start, rate): + task = generate_task() + task.di_channels.add_di_chan( + flatten_channel_string( + sim_6363_device.di_lines.channel_names[lines_start : lines_start + 4] + ), + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, + ) + task.timing.cfg_samp_clk_timing(rate, sample_mode=AcquisitionType.FINITE, samps_per_chan=10) + return DigitalSingleChannelReader(task.in_stream) + + sample_count = 10 + signal_count = 4 + reader0 = _make_single_channel_multi_line_reader(lines_start=0, rate=1000.0) + reader1 = _make_single_channel_multi_line_reader(lines_start=1, rate=2000.0) + waveform = DigitalWaveform(sample_count, signal_count) + + reader0.read_waveform(waveform, sample_count) + timestamp0 = waveform.timing.timestamp + assert _get_waveform_data(waveform) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == f"{sim_6363_device.di_lines[0].name}..." + + reader1.read_waveform(waveform, sample_count) + timestamp1 = waveform.timing.timestamp + assert _get_waveform_data(waveform) == [0, 0, 1, 1, 2, 2, 3, 3, 4, 4] + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 2000) + assert waveform.channel_name == f"{sim_6363_device.di_lines[1].name}..." + + assert timestamp1 > timestamp0 + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader___read_into_undersized_waveform___throws_exception( + di_single_line_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) + samples_to_read = 10 + + waveform = DigitalWaveform(samples_to_read - 1) + with pytest.raises(DaqError) as exc_info: + reader.read_waveform(waveform, samples_to_read) + + assert exc_info.value.error_code == DAQmxErrors.READ_BUFFER_TOO_SMALL + assert exc_info.value.args[0].startswith("Buffer is too small to fit read data.") + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader___read_waveform_high_sample_rate___returns_correct_sample_interval( + di_single_line_high_rate_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_line_high_rate_task.in_stream) + samples_to_read = 50 + waveform = DigitalWaveform(samples_to_read) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 10_000_000) + assert waveform.sample_count == samples_to_read + assert waveform.channel_name == di_single_line_high_rate_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader_with_timing_flag___read_waveform___only_includes_timing_data( + di_single_line_timing_task: nidaqmx.Task, +) -> None: + in_stream = di_single_line_timing_task.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.TIMING + reader = DigitalSingleChannelReader(in_stream) + waveform = DigitalWaveform(50) + + samples_read = reader.read_waveform(waveform) + + assert samples_read == 50 + assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == "" + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader_with_extended_properties_flag___read_waveform___only_includes_extended_properties( + di_single_line_timing_task: nidaqmx.Task, +) -> None: + in_stream = di_single_line_timing_task.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.EXTENDED_PROPERTIES + reader = DigitalSingleChannelReader(in_stream) + waveform = DigitalWaveform(50) + + samples_read = reader.read_waveform(waveform) + + assert samples_read == 50 + assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE + assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader_with_both_flags___read_waveform___includes_both_timing_and_extended_properties( + di_single_line_timing_task: nidaqmx.Task, +) -> None: + in_stream = di_single_line_timing_task.in_stream + in_stream.waveform_attribute_mode = ( + WaveformAttributeMode.TIMING | WaveformAttributeMode.EXTENDED_PROPERTIES + ) + reader = DigitalSingleChannelReader(in_stream) + waveform = DigitalWaveform(50) + + samples_read = reader.read_waveform(waveform) + + assert samples_read == 50 + assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_line_reader_with_none_flag___read_waveform___minimal_waveform_data( + di_single_line_timing_task: nidaqmx.Task, +) -> None: + in_stream = di_single_line_timing_task.in_stream + in_stream.waveform_attribute_mode = WaveformAttributeMode.NONE + reader = DigitalSingleChannelReader(in_stream) + waveform = DigitalWaveform(50) + + samples_read = reader.read_waveform(waveform) + + assert samples_read == 50 + assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE + assert waveform.channel_name == "" + + +@pytest.mark.xfail( + reason="TODO: AB#3178052 - DigitalWaveform signal index is reversed when channels are specified by ports", + raises=AssertionError, +) +@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") +def test___digital_single_channel_port_uint32_reader___read_waveform___returns_valid_waveform( + di_single_channel_port_uint32_timing_task: nidaqmx.Task, +) -> None: + reader = DigitalSingleChannelReader(di_single_channel_port_uint32_timing_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_channel_port_uint32_timing_task) + samples_to_read = 10 + waveform = DigitalWaveform(samples_to_read, num_lines) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == 50 + assert _get_waveform_data(waveform) == _get_expected_digital_data(num_lines, samples_to_read) + assert _is_timestamp_close_to_now(waveform.timing.timestamp) + assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) + assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR + assert waveform.channel_name == di_single_channel_port_uint32_timing_task.di_channels[0].name + + +@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") +def test___digital_single_channel_lines_and_port___read_waveform___returns_valid_waveform( + di_single_chan_lines_and_port_task: nidaqmx.Task, + sim_6363_device: nidaqmx.system.Device, +) -> None: + reader = DigitalSingleChannelReader(di_single_chan_lines_and_port_task.in_stream) + num_lines = _get_num_lines_in_task(di_single_chan_lines_and_port_task) + samples_to_read = 10 + waveform = DigitalWaveform(samples_to_read, num_lines) + + samples_read = reader.read_waveform(waveform, samples_to_read) + + assert samples_read == samples_to_read + # Note, the data on the port's waveform is MSB instead of LSB because of bug AB#3178052 + # When that bug is fixed, these asserts should be updated + assert _get_waveform_data(waveform) == [0, 1025, 514, 1539, 260, 1285, 774, 1799, 128, 1153] + assert waveform.sample_count == samples_to_read + assert waveform.channel_name == di_single_chan_lines_and_port_task.di_channels[0].name + assert waveform._get_signal_names() == [ + sim_6363_device.di_lines[0].name, + sim_6363_device.di_lines[1].name, + sim_6363_device.di_lines[2].name, + sim_6363_device.di_lines[39].name, + sim_6363_device.di_lines[38].name, + sim_6363_device.di_lines[37].name, + sim_6363_device.di_lines[36].name, + sim_6363_device.di_lines[35].name, + sim_6363_device.di_lines[34].name, + sim_6363_device.di_lines[33].name, + sim_6363_device.di_lines[32].name, + ] diff --git a/tests/component/readers/test_power_readers_ai.py b/tests/component/readers/test_power_readers_ai.py new file mode 100644 index 000000000..1e82a0687 --- /dev/null +++ b/tests/component/readers/test_power_readers_ai.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import ctypes +import math + +import numpy +import numpy.typing +import pytest + +import nidaqmx +from nidaqmx.stream_readers import ( + PowerBinaryReader, + PowerMultiChannelReader, + PowerSingleChannelReader, +) +from .conftest import ( + POWER_BINARY_EPSILON, + POWER_EPSILON, + _get_current_code_setpoint_for_chan, + _get_current_setpoint_for_chan, + _get_voltage_code_setpoint_for_chan, + _get_voltage_setpoint_for_chan, +) + + +def test___power_single_channel_reader___read_one_sample___returns_valid_samples( + pwr_single_channel_task: nidaqmx.Task, +) -> None: + reader = PowerSingleChannelReader(pwr_single_channel_task.in_stream) + + data = reader.read_one_sample() + + assert data.voltage == pytest.approx(_get_voltage_setpoint_for_chan(0), abs=POWER_EPSILON) + assert data.current == pytest.approx(_get_current_setpoint_for_chan(0), abs=POWER_EPSILON) + + +def test___power_single_channel_reader___read_many_sample___returns_valid_samples( + pwr_single_channel_task: nidaqmx.Task, +) -> None: + reader = PowerSingleChannelReader(pwr_single_channel_task.in_stream) + samples_to_read = 10 + voltage_data = numpy.full(samples_to_read, math.inf, dtype=numpy.float64) + current_data = numpy.full(samples_to_read, math.inf, dtype=numpy.float64) + + samples_read = reader.read_many_sample(voltage_data, current_data, samples_to_read) + + assert samples_read == samples_to_read + assert voltage_data == pytest.approx(_get_voltage_setpoint_for_chan(0), abs=POWER_EPSILON) + assert current_data == pytest.approx(_get_current_setpoint_for_chan(0), abs=POWER_EPSILON) + + +@pytest.mark.parametrize( + "voltage_dtype, current_dtype", + [ + (numpy.float32, numpy.float64), + (numpy.float64, numpy.float32), + (numpy.float32, numpy.float32), + ], +) +def test___power_single_channel_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( + pwr_single_channel_task: nidaqmx.Task, + voltage_dtype: numpy.typing.DTypeLike, + current_dtype: numpy.typing.DTypeLike, +) -> None: + reader = PowerSingleChannelReader(pwr_single_channel_task.in_stream) + samples_to_read = 10 + voltage_data = numpy.full(samples_to_read, math.inf, dtype=voltage_dtype) + current_data = numpy.full(samples_to_read, math.inf, dtype=current_dtype) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample(voltage_data, current_data, samples_to_read) + + assert "float64" in exc_info.value.args[0] + + +def test___power_multi_channel_reader___read_one_sample___returns_valid_samples( + pwr_multi_channel_task: nidaqmx.Task, +) -> None: + reader = PowerMultiChannelReader(pwr_multi_channel_task.in_stream) + num_channels = pwr_multi_channel_task.number_of_channels + voltage_data = numpy.full(num_channels, math.inf, dtype=numpy.float64) + current_data = numpy.full(num_channels, math.inf, dtype=numpy.float64) + + reader.read_one_sample(voltage_data, current_data) + + assert voltage_data == pytest.approx( + [_get_voltage_setpoint_for_chan(chan_index) for chan_index in range(num_channels)], + abs=POWER_EPSILON, + ) + assert current_data == pytest.approx( + [_get_current_setpoint_for_chan(chan_index) for chan_index in range(num_channels)], + abs=POWER_EPSILON, + ) + + +@pytest.mark.parametrize( + "voltage_dtype, current_dtype", + [ + (numpy.float32, numpy.float64), + (numpy.float64, numpy.float32), + (numpy.float32, numpy.float32), + ], +) +def test___power_multi_channel_reader___read_one_sample_with_wrong_dtype___raises_error_with_correct_dtype( + pwr_multi_channel_task: nidaqmx.Task, + voltage_dtype: numpy.typing.DTypeLike, + current_dtype: numpy.typing.DTypeLike, +) -> None: + reader = PowerMultiChannelReader(pwr_multi_channel_task.in_stream) + num_channels = pwr_multi_channel_task.number_of_channels + voltage_data = numpy.full(num_channels, math.inf, dtype=voltage_dtype) + current_data = numpy.full(num_channels, math.inf, dtype=current_dtype) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + reader.read_one_sample(voltage_data, current_data) + + assert "float64" in exc_info.value.args[0] + + +def test___power_multi_channel_reader___read_many_sample___returns_valid_samples( + pwr_multi_channel_task: nidaqmx.Task, +) -> None: + reader = PowerMultiChannelReader(pwr_multi_channel_task.in_stream) + num_channels = pwr_multi_channel_task.number_of_channels + samples_to_read = 10 + voltage_data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) + current_data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) + + samples_read = reader.read_many_sample( + voltage_data, current_data, number_of_samples_per_channel=samples_to_read + ) + + assert samples_read == samples_to_read + expected_voltage_vals = [ + _get_voltage_setpoint_for_chan(chan_index) for chan_index in range(num_channels) + ] + expected_current_vals = [ + _get_current_setpoint_for_chan(chan_index) for chan_index in range(num_channels) + ] + assert voltage_data == pytest.approx(expected_voltage_vals, abs=POWER_EPSILON) + assert current_data == pytest.approx(expected_current_vals, abs=POWER_EPSILON) + + +@pytest.mark.parametrize( + "voltage_dtype, current_dtype", + [ + (numpy.float32, numpy.float64), + (numpy.float64, numpy.float32), + (numpy.float32, numpy.float32), + ], +) +def test___power_multi_channel_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( + pwr_multi_channel_task: nidaqmx.Task, + voltage_dtype: numpy.typing.DTypeLike, + current_dtype: numpy.typing.DTypeLike, +) -> None: + reader = PowerMultiChannelReader(pwr_multi_channel_task.in_stream) + num_channels = pwr_multi_channel_task.number_of_channels + samples_to_read = 10 + voltage_data = numpy.full((num_channels, samples_to_read), math.inf, dtype=voltage_dtype) + current_data = numpy.full((num_channels, samples_to_read), math.inf, dtype=current_dtype) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample( + voltage_data, current_data, number_of_samples_per_channel=samples_to_read + ) + + assert "float64" in exc_info.value.args[0] + + +def test___power_binary_reader___read_many_sample___returns_valid_samples( + pwr_multi_channel_task: nidaqmx.Task, +) -> None: + reader = PowerBinaryReader(pwr_multi_channel_task.in_stream) + num_channels = pwr_multi_channel_task.number_of_channels + samples_to_read = 10 + voltage_data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.int16).min, dtype=numpy.int16 + ) + current_data = numpy.full( + (num_channels, samples_to_read), numpy.iinfo(numpy.int16).min, dtype=numpy.int16 + ) + + samples_read = reader.read_many_sample( + voltage_data, current_data, number_of_samples_per_channel=samples_to_read + ) + + assert samples_read == samples_to_read + expected_voltage_vals = [ + _get_voltage_code_setpoint_for_chan(chan_index) for chan_index in range(num_channels) + ] + expected_current_vals = [ + _get_current_code_setpoint_for_chan(chan_index) for chan_index in range(num_channels) + ] + assert voltage_data == pytest.approx(expected_voltage_vals, abs=POWER_BINARY_EPSILON) + assert current_data == pytest.approx(expected_current_vals, abs=POWER_BINARY_EPSILON) + + +@pytest.mark.parametrize( + "voltage_dtype, voltage_default, current_dtype, current_default", + [ + (numpy.float64, math.inf, numpy.int16, numpy.iinfo(numpy.int16).min), + (numpy.int16, numpy.iinfo(numpy.int16).min, numpy.float64, math.inf), + (numpy.float64, math.inf, numpy.float64, math.inf), + ], +) +def test___power_binary_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( + pwr_multi_channel_task: nidaqmx.Task, + voltage_dtype: numpy.typing.DTypeLike, + voltage_default: float | int, + current_dtype: numpy.typing.DTypeLike, + current_default: float | int, +) -> None: + reader = PowerBinaryReader(pwr_multi_channel_task.in_stream) + num_channels = pwr_multi_channel_task.number_of_channels + samples_to_read = 10 + voltage_data = numpy.full((num_channels, samples_to_read), voltage_default, dtype=voltage_dtype) + current_data = numpy.full((num_channels, samples_to_read), current_default, dtype=current_dtype) + + with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: + _ = reader.read_many_sample( + voltage_data, current_data, number_of_samples_per_channel=samples_to_read + ) + + assert "int16" in exc_info.value.args[0] diff --git a/tests/component/test_task_read_ai.py b/tests/component/readers/test_task_read_ai.py similarity index 77% rename from tests/component/test_task_read_ai.py rename to tests/component/readers/test_task_read_ai.py index ccddb0990..f7dc50ec3 100644 --- a/tests/component/test_task_read_ai.py +++ b/tests/component/readers/test_task_read_ai.py @@ -3,50 +3,15 @@ import pytest import nidaqmx -import nidaqmx.system from nidaqmx.constants import AcquisitionType - - -# Simulated DAQ voltage data is a noisy sinewave within the range of the minimum and maximum values -# of the virtual channel. We can leverage this behavior to validate we get the correct data from -# the Python bindings. -def _get_voltage_offset_for_chan(chan_index: int) -> float: - return float(chan_index + 1) - - -VOLTAGE_EPSILON = 1e-3 - - -@pytest.fixture -def ai_single_channel_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - offset = _get_voltage_offset_for_chan(0) - task.ai_channels.add_ai_voltage_chan( - sim_6363_device.ai_physical_chans[0].name, - min_val=offset, - max_val=offset + VOLTAGE_EPSILON, - ) - return task - - -@pytest.fixture -def ai_multi_channel_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - for chan_index in range(3): - offset = _get_voltage_offset_for_chan(chan_index) - chan = task.ai_channels.add_ai_voltage_chan( - sim_6363_device.ai_physical_chans[chan_index].name, - min_val=offset, - # min and max must be different, so add a small epsilon - max_val=offset + VOLTAGE_EPSILON, - ) - # forcing the maximum range for binary read scaling to be predictable - chan.ai_rng_high = 10 - chan.ai_rng_low = -10 - - return task +from .conftest import ( + POWER_EPSILON, + VOLTAGE_EPSILON, + _assert_equal_2d, + _get_current_setpoint_for_chan, + _get_voltage_offset_for_chan, + _get_voltage_setpoint_for_chan, +) def test___analog_single_channel___read_unset_samples___returns_valid_scalar( @@ -153,51 +118,6 @@ def test___analog_multi_channel_finite___read_too_many_sample___returns_valid_2d _assert_equal_2d(data, expected, abs=VOLTAGE_EPSILON) -def _assert_equal_2d(data: list[list[float]], expected: list[list[float]], abs: float) -> None: - # pytest.approx() does not support nested data structures. - assert len(data) == len(expected) - for i in range(len(data)): - assert data[i] == pytest.approx(expected[i], abs=abs) - - -POWER_EPSILON = 1e-3 - - -def _get_voltage_setpoint_for_chan(chan_index: int) -> float: - return float(chan_index + 1) - - -def _get_current_setpoint_for_chan(chan_index: int) -> float: - return float(chan_index + 1) - - -@pytest.fixture -def pwr_single_channel_task( - task: nidaqmx.Task, sim_ts_power_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.ai_channels.add_ai_power_chan( - f"{sim_ts_power_device.name}/power", - _get_voltage_setpoint_for_chan(0), - _get_current_setpoint_for_chan(0), - True, # output enable - ) - return task - - -@pytest.fixture -def pwr_multi_channel_task( - task: nidaqmx.Task, sim_ts_power_devices: list[nidaqmx.system.Device] -) -> nidaqmx.Task: - for chan_index, sim_ts_power_device in enumerate(sim_ts_power_devices): - task.ai_channels.add_ai_power_chan( - f"{sim_ts_power_device.name}/power", - _get_voltage_setpoint_for_chan(chan_index), - _get_current_setpoint_for_chan(chan_index), - True, # output enable - ) - return task - - def test___power_single_channel___read_unset_samples___returns_valid_scalar( pwr_single_channel_task: nidaqmx.Task, ) -> None: diff --git a/tests/component/test_task_read_waveform_ai.py b/tests/component/readers/test_task_read_waveform_ai.py similarity index 74% rename from tests/component/test_task_read_waveform_ai.py rename to tests/component/readers/test_task_read_waveform_ai.py index 22024ee7f..2989ff552 100644 --- a/tests/component/test_task_read_waveform_ai.py +++ b/tests/component/readers/test_task_read_waveform_ai.py @@ -4,58 +4,17 @@ from nitypes.waveform import AnalogWaveform import nidaqmx -import nidaqmx.system -from nidaqmx.constants import AcquisitionType - - -# Simulated DAQ voltage data is a noisy sinewave within the range of the minimum and maximum values -# of the virtual channel. We can leverage this behavior to validate we get the correct data from -# the Python bindings. -def _get_voltage_offset_for_chan(chan_index: int) -> float: - return float(chan_index + 1) - - -VOLTAGE_EPSILON = 1e-3 - - -@pytest.fixture -def ai_single_channel_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - offset = _get_voltage_offset_for_chan(0) - task.ai_channels.add_ai_voltage_chan( - sim_6363_device.ai_physical_chans[0].name, - min_val=offset, - max_val=offset + VOLTAGE_EPSILON, - ) - task.timing.cfg_samp_clk_timing(1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50) - return task - - -@pytest.fixture -def ai_multi_channel_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - for chan_index in range(3): - offset = _get_voltage_offset_for_chan(chan_index) - chan = task.ai_channels.add_ai_voltage_chan( - sim_6363_device.ai_physical_chans[chan_index].name, - min_val=offset, - # min and max must be different, so add a small epsilon - max_val=offset + VOLTAGE_EPSILON, - ) - # forcing the maximum range for binary read scaling to be predictable - chan.ai_rng_high = 10 - chan.ai_rng_low = -10 - task.timing.cfg_samp_clk_timing(1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50) - return task +from .conftest import ( + VOLTAGE_EPSILON, + _get_voltage_offset_for_chan, +) @pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") def test___analog_single_channel___read_waveform___returns_valid_waveform( - ai_single_channel_task: nidaqmx.Task, + ai_single_channel_task_with_timing: nidaqmx.Task, ) -> None: - waveform = ai_single_channel_task.read_waveform() + waveform = ai_single_channel_task_with_timing.read_waveform() assert isinstance(waveform, AnalogWaveform) expected = _get_voltage_offset_for_chan(0) @@ -110,11 +69,11 @@ def test___analog_single_channel_finite___read_waveform_too_many_samples___retur @pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") def test___analog_multi_channel___read_waveform___returns_valid_waveforms( - ai_multi_channel_task: nidaqmx.Task, + ai_multi_channel_task_with_timing: nidaqmx.Task, ) -> None: - num_channels = ai_multi_channel_task.number_of_channels + num_channels = ai_multi_channel_task_with_timing.number_of_channels - waveforms = ai_multi_channel_task.read_waveform() + waveforms = ai_multi_channel_task_with_timing.read_waveform() assert isinstance(waveforms, list) assert len(waveforms) == num_channels diff --git a/tests/component/test_task_read_waveform_di.py b/tests/component/readers/test_task_read_waveform_di.py similarity index 72% rename from tests/component/test_task_read_waveform_di.py rename to tests/component/readers/test_task_read_waveform_di.py index 75a304356..d191c0dd2 100644 --- a/tests/component/test_task_read_waveform_di.py +++ b/tests/component/readers/test_task_read_waveform_di.py @@ -1,120 +1,14 @@ from __future__ import annotations -import numpy import pytest from nitypes.waveform import DigitalWaveform import nidaqmx import nidaqmx.system -from nidaqmx.constants import AcquisitionType, LineGrouping -from nidaqmx.utils import flatten_channel_string - - -@pytest.fixture -def di_single_channel_timing_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - sim_6363_device.di_lines[0].name, line_grouping=LineGrouping.CHAN_FOR_ALL_LINES - ) - task.timing.cfg_samp_clk_timing(1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50) - return task - - -@pytest.fixture -def di_single_chan_lines_and_port_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - flatten_channel_string( - sim_6363_device.di_lines.channel_names[0:3] + [sim_6363_device.di_ports[1].name] - ), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -@pytest.fixture -def di_multi_channel_timing_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[:8]), - line_grouping=LineGrouping.CHAN_PER_LINE, - ) - task.timing.cfg_samp_clk_timing(1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50) - return task - - -@pytest.fixture -def di_multi_chan_diff_lines_timing_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[0:1]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[1:3]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[3:7]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.timing.cfg_samp_clk_timing( - rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return task - - -@pytest.fixture -def di_multi_chan_lines_and_port_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[0:1]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[1:3]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[3:7]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - sim_6363_device.di_ports[1].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -def _get_expected_data_for_line(num_samples: int, line_number: int) -> list[int]: - data = [] - # Simulated digital signals "count" from 0 in binary within each group of 8 lines. - # Each line represents a bit in the binary representation of the sample number. - # - line 0 represents bit 0 (LSB) - alternates every sample: 0,1,0,1,0,1,0,1... - # - line 1 represents bit 1 - alternates every 2 samples: 0,0,1,1,0,0,1,1... - # - line 2 represents bit 2 - alternates every 4 samples: 0,0,0,0,1,1,1,1... - line_number %= 8 - for sample_num in range(num_samples): - bit_value = (sample_num >> line_number) & 1 - data.append(bit_value) - return data - - -def _bool_array_to_int(bool_array: numpy.typing.NDArray[numpy.bool_]) -> int: - result = 0 - # Simulated data is little-endian - for bit in bool_array[::-1]: - result = (result << 1) | int(bit) - return result - - -def _get_waveform_data(waveform: DigitalWaveform) -> list[int]: - return [_bool_array_to_int(sample) for sample in waveform.data] +from .conftest import ( + _get_expected_data_for_line, + _get_waveform_data, +) @pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") diff --git a/tests/component/test_stream_readers_ai.py b/tests/component/test_stream_readers_ai.py deleted file mode 100644 index d25262382..000000000 --- a/tests/component/test_stream_readers_ai.py +++ /dev/null @@ -1,1118 +0,0 @@ -from __future__ import annotations - -import ctypes -import math -from datetime import timezone -from typing import Callable - -import numpy -import numpy.typing -import pytest -from hightime import datetime as ht_datetime, timedelta as ht_timedelta -from nitypes.waveform import AnalogWaveform, SampleIntervalMode - -import nidaqmx -import nidaqmx.system -from nidaqmx._feature_toggles import WAVEFORM_SUPPORT, FeatureNotSupportedError -from nidaqmx.constants import AcquisitionType, WaveformAttributeMode -from nidaqmx.error_codes import DAQmxErrors -from nidaqmx.stream_readers import ( - AnalogMultiChannelReader, - AnalogSingleChannelReader, - AnalogUnscaledReader, - DaqError, - PowerBinaryReader, - PowerMultiChannelReader, - PowerSingleChannelReader, -) - - -# Simulated DAQ voltage data is a noisy sinewave within the range of the minimum and maximum values -# of the virtual channel. We can leverage this behavior to validate we get the correct data from -# the Python bindings. -def _get_voltage_offset_for_chan(chan_index: int) -> float: - return float(chan_index + 1) - - -def _volts_to_codes(volts: float, max_code: int = 32767, max_voltage: float = 10.0) -> int: - return int(volts * max_code / max_voltage) - - -# Note: Since we only use positive voltages, this works fine for both signed and unsigned reads. -def _get_voltage_code_offset_for_chan(chan_index: int) -> int: - voltage_limits = _get_voltage_offset_for_chan(chan_index) - return _volts_to_codes(voltage_limits) - - -def _is_timestamp_close_to_now(timestamp: ht_datetime, tolerance_seconds: float = 1.0) -> bool: - current_time = ht_datetime.now(timezone.utc) - time_diff = abs((timestamp - current_time).total_seconds()) - return time_diff <= tolerance_seconds - - -VOLTAGE_EPSILON = 1e-3 -VOLTAGE_CODE_EPSILON = round(_volts_to_codes(VOLTAGE_EPSILON)) - - -@pytest.fixture -def ai_single_channel_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - offset = _get_voltage_offset_for_chan(0) - task.ai_channels.add_ai_voltage_chan( - sim_6363_device.ai_physical_chans[0].name, - min_val=offset, - max_val=offset + VOLTAGE_EPSILON, - ) - return task - - -@pytest.fixture -def ai_single_channel_task_with_timing( - ai_single_channel_task: nidaqmx.Task, -) -> nidaqmx.Task: - # Configure timing for waveform reading - ai_single_channel_task.timing.cfg_samp_clk_timing( - 1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return ai_single_channel_task - - -@pytest.fixture -def ai_single_channel_task_with_high_rate( - task: nidaqmx.Task, sim_charge_device: nidaqmx.system.Device -) -> nidaqmx.Task: - offset = _get_voltage_offset_for_chan(0) - task.ai_channels.add_ai_voltage_chan( - sim_charge_device.ai_physical_chans[0].name, - min_val=offset, - max_val=offset + VOLTAGE_EPSILON, - ) - task.timing.cfg_samp_clk_timing( - rate=10_000_000, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return task - - -@pytest.fixture -def ai_multi_channel_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - for chan_index in range(3): - offset = _get_voltage_offset_for_chan(chan_index) - chan = task.ai_channels.add_ai_voltage_chan( - sim_6363_device.ai_physical_chans[chan_index].name, - min_val=offset, - # min and max must be different, so add a small epsilon - max_val=offset + VOLTAGE_EPSILON, - ) - # forcing the maximum range for binary read scaling to be predictable - chan.ai_rng_high = 10 - chan.ai_rng_low = -10 - - return task - - -@pytest.fixture -def ai_multi_channel_task_with_timing( - ai_multi_channel_task: nidaqmx.Task, -) -> nidaqmx.Task: - # Configure timing for waveform reading - ai_multi_channel_task.timing.cfg_samp_clk_timing( - 1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return ai_multi_channel_task - - -def test___analog_single_channel_reader___read_one_sample___returns_valid_samples( - ai_single_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogSingleChannelReader(ai_single_channel_task.in_stream) - - data = reader.read_one_sample() - - expected = _get_voltage_offset_for_chan(0) - assert data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - - -def test___analog_single_channel_reader___read_many_sample___returns_valid_samples( - ai_single_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogSingleChannelReader(ai_single_channel_task.in_stream) - samples_to_read = 10 - data = numpy.full(samples_to_read, math.inf, dtype=numpy.float64) - - samples_read = reader.read_many_sample(data, samples_to_read) - - assert samples_read == samples_to_read - expected = _get_voltage_offset_for_chan(0) - assert data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - - -def test___analog_single_channel_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( - ai_single_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogSingleChannelReader(ai_single_channel_task.in_stream) - samples_to_read = 10 - data = numpy.full(samples_to_read, math.inf, dtype=numpy.float32) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample(data, samples_to_read) - - assert "float64" in exc_info.value.args[0] - - -@pytest.mark.disable_feature_toggle(WAVEFORM_SUPPORT) -def test___analog_single_channel_reader___read_waveform_feature_disabled___raises_feature_not_supported_error( - ai_single_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) - waveform = AnalogWaveform(50) - - with pytest.raises(FeatureNotSupportedError) as exc_info: - reader.read_waveform(waveform) - - error_message = str(exc_info.value) - assert "WAVEFORM_SUPPORT feature is not supported" in error_message - assert "NIDAQMX_ENABLE_WAVEFORM_SUPPORT" in error_message - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader___read_waveform___returns_valid_waveform( - ai_single_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) - samples_to_read = 10 - waveform = AnalogWaveform(samples_to_read) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(0) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name - assert waveform.units == "Volts" - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader___read_waveform_no_args___returns_valid_waveform( - ai_single_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) - waveform = AnalogWaveform(50) - - samples_read = reader.read_waveform(waveform) - - assert samples_read == 50 - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(0) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name - assert waveform.units == "Volts" - assert waveform.sample_count == 50 - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader___read_waveform_in_place___populates_valid_waveform( - ai_single_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) - samples_to_read = 10 - - waveform = AnalogWaveform(samples_to_read) - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(0) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name - assert waveform.units == "Volts" - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader___reuse_waveform_in_place___overwrites_data_timing_and_attributes( - generate_task: Callable[[], nidaqmx.Task], sim_6363_device: nidaqmx.system.Device -) -> None: - def _make_single_channel_reader(chan_index, offset, rate): - task = generate_task() - task.ai_channels.add_ai_voltage_chan( - sim_6363_device.ai_physical_chans[chan_index].name, - min_val=offset, - max_val=offset + VOLTAGE_EPSILON, - ) - task.timing.cfg_samp_clk_timing(rate, sample_mode=AcquisitionType.FINITE, samps_per_chan=10) - return AnalogSingleChannelReader(task.in_stream) - - reader0 = _make_single_channel_reader(chan_index=0, offset=0, rate=1000.0) - reader1 = _make_single_channel_reader(chan_index=1, offset=1, rate=2000.0) - waveform = AnalogWaveform(10) - - reader0.read_waveform(waveform, 10) - timestamp1 = waveform.timing.timestamp - assert waveform.scaled_data == pytest.approx(0, abs=VOLTAGE_EPSILON) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == f"{sim_6363_device.name}/ai0" - - reader1.read_waveform(waveform, 10) - timestamp2 = waveform.timing.timestamp - assert waveform.scaled_data == pytest.approx(1, abs=VOLTAGE_EPSILON) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 2000) - assert waveform.channel_name == f"{sim_6363_device.name}/ai1" - - assert timestamp2 > timestamp1 - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader___read_into_undersized_waveform___throws_exception( - ai_single_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogSingleChannelReader(ai_single_channel_task_with_timing.in_stream) - samples_to_read = 10 - - waveform = AnalogWaveform(samples_to_read - 1) - with pytest.raises(DaqError) as exc_info: - reader.read_waveform(waveform, samples_to_read) - - assert exc_info.value.error_code == DAQmxErrors.READ_BUFFER_TOO_SMALL - assert exc_info.value.args[0].startswith("The provided waveform does not have enough space") - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader___read_waveform_high_sample_rate___returns_correct_sample_interval( - ai_single_channel_task_with_high_rate: nidaqmx.Task, -) -> None: - reader = AnalogSingleChannelReader(ai_single_channel_task_with_high_rate.in_stream) - samples_to_read = 50 - waveform = AnalogWaveform(samples_to_read) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(0) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 10_000_000) - assert waveform.channel_name == ai_single_channel_task_with_high_rate.ai_channels[0].name - assert waveform.units == "Volts" - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader_with_timing_flag___read_waveform___only_includes_timing_data( - ai_single_channel_task_with_timing: nidaqmx.Task, -) -> None: - in_stream = ai_single_channel_task_with_timing.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.TIMING - reader = AnalogSingleChannelReader(in_stream) - samples_to_read = 10 - waveform = AnalogWaveform(samples_to_read) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert isinstance(waveform, AnalogWaveform) - assert waveform.sample_count == samples_to_read - expected = _get_voltage_offset_for_chan(0) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == "" - assert waveform.units == "" - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader_with_extended_properties_flag___read_waveform___only_includes_extended_properties( - ai_single_channel_task_with_timing: nidaqmx.Task, -) -> None: - in_stream = ai_single_channel_task_with_timing.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.EXTENDED_PROPERTIES - reader = AnalogSingleChannelReader(in_stream) - samples_to_read = 10 - waveform = AnalogWaveform(samples_to_read) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert isinstance(waveform, AnalogWaveform) - assert waveform.sample_count == samples_to_read - expected = _get_voltage_offset_for_chan(0) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE - assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name - assert waveform.units == "Volts" - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader_with_both_flags___read_waveform___includes_both_timing_and_extended_properties( - ai_single_channel_task_with_timing: nidaqmx.Task, -) -> None: - in_stream = ai_single_channel_task_with_timing.in_stream - in_stream.waveform_attribute_mode = ( - WaveformAttributeMode.TIMING | WaveformAttributeMode.EXTENDED_PROPERTIES - ) - reader = AnalogSingleChannelReader(in_stream) - samples_to_read = 10 - waveform = AnalogWaveform(samples_to_read) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert isinstance(waveform, AnalogWaveform) - assert waveform.sample_count == samples_to_read - expected = _get_voltage_offset_for_chan(0) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == ai_single_channel_task_with_timing.ai_channels[0].name - assert waveform.units == "Volts" - - -@pytest.mark.grpc_skip(reason="read_analog_waveform not implemented in GRPC") -def test___analog_single_channel_reader_with_none_flag___read_waveform___minimal_waveform_data( - ai_single_channel_task_with_timing: nidaqmx.Task, -) -> None: - in_stream = ai_single_channel_task_with_timing.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.NONE - reader = AnalogSingleChannelReader(in_stream) - samples_to_read = 10 - waveform = AnalogWaveform(samples_to_read) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert isinstance(waveform, AnalogWaveform) - assert waveform.sample_count == samples_to_read - expected = _get_voltage_offset_for_chan(0) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE - assert waveform.channel_name == "" - assert waveform.units == "" - - -def test___analog_multi_channel_reader___read_one_sample___returns_valid_samples( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - data = numpy.full(num_channels, math.inf, dtype=numpy.float64) - - reader.read_one_sample(data) - - expected = [_get_voltage_offset_for_chan(chan_index) for chan_index in range(num_channels)] - assert data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - - -def test___analog_multi_channel_reader___read_one_sample_with_wrong_dtype___raises_error_with_correct_dtype( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - data = numpy.full(num_channels, math.inf, dtype=numpy.float32) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - reader.read_one_sample(data) - - assert "float64" in exc_info.value.args[0] - - -def test___analog_multi_channel_reader___read_many_sample___returns_valid_samples( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) - - samples_read = reader.read_many_sample(data, samples_to_read) - - assert samples_read == samples_to_read - expected_vals = [_get_voltage_offset_for_chan(chan_index) for chan_index in range(num_channels)] - assert data == pytest.approx(expected_vals, abs=VOLTAGE_EPSILON) - - -def test___analog_multi_channel_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float32) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample(data, samples_to_read) - - assert "float64" in exc_info.value.args[0] - - -@pytest.mark.disable_feature_toggle(WAVEFORM_SUPPORT) -def test___analog_multi_channel_reader___read_waveforms_feature_disabled___raises_feature_not_supported_error( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) - num_channels = ai_multi_channel_task_with_timing.number_of_channels - samples_to_read = 10 - waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] - - with pytest.raises(FeatureNotSupportedError) as exc_info: - reader.read_waveforms(waveforms) - - error_message = str(exc_info.value) - assert "WAVEFORM_SUPPORT feature is not supported" in error_message - assert "NIDAQMX_ENABLE_WAVEFORM_SUPPORT" in error_message - - -@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") -def test___analog_multi_channel_reader___read_waveforms___returns_valid_waveforms( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) - num_channels = ai_multi_channel_task_with_timing.number_of_channels - samples_to_read = 10 - waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 3 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan_index, waveform in enumerate(waveforms): - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(chan_index) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert ( - waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name - ) - assert waveform.units == "Volts" - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") -def test___analog_multi_channel_reader___read_waveforms_no_args___returns_valid_waveforms( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) - num_channels = ai_multi_channel_task_with_timing.number_of_channels - waveforms = [AnalogWaveform(50) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms) - - assert samples_read == 50 - assert num_channels == 3 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan_index, waveform in enumerate(waveforms): - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(chan_index) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert ( - waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name - ) - assert waveform.units == "Volts" - assert waveform.sample_count == 50 - - -@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") -def test___analog_multi_channel_reader___read_waveforms_in_place___populates_valid_waveforms( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) - num_channels = ai_multi_channel_task_with_timing.number_of_channels - samples_to_read = 10 - - waveforms = [ - AnalogWaveform(samples_to_read), - AnalogWaveform(samples_to_read), - AnalogWaveform(samples_to_read), - ] - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 3 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan_index, waveform in enumerate(waveforms): - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(chan_index) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert ( - waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name - ) - assert waveform.units == "Volts" - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") -def test___analog_multi_channel_reader___read_into_undersized_waveforms___throws_exception( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) - samples_to_read = 10 - - waveforms = [ - AnalogWaveform(samples_to_read), - AnalogWaveform(samples_to_read - 1), - AnalogWaveform(samples_to_read), - ] - with pytest.raises(DaqError) as exc_info: - reader.read_waveforms(waveforms, samples_to_read) - - assert exc_info.value.error_code == DAQmxErrors.READ_BUFFER_TOO_SMALL - assert exc_info.value.args[0].startswith("The waveform at index 1 does not have enough space") - - -@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") -def test___analog_multi_channel_reader___read_with_wrong_number_of_waveforms___throws_exception( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - reader = AnalogMultiChannelReader(ai_multi_channel_task_with_timing.in_stream) - samples_to_read = 10 - - waveforms = [ - AnalogWaveform(samples_to_read), - AnalogWaveform(samples_to_read), - ] - with pytest.raises(DaqError) as exc_info: - reader.read_waveforms(waveforms, samples_to_read) - - assert exc_info.value.error_code == DAQmxErrors.MISMATCHED_INPUT_ARRAY_SIZES - assert "does not match the number of channels" in exc_info.value.args[0] - - -@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") -def test___analog_multi_channel_reader_with_timing_flag___read_waveforms___only_includes_timing_data( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - in_stream = ai_multi_channel_task_with_timing.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.TIMING - reader = AnalogMultiChannelReader(in_stream) - num_channels = ai_multi_channel_task_with_timing.number_of_channels - samples_to_read = 10 - waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 3 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan_index, waveform in enumerate(waveforms): - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(chan_index) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == "" - assert waveform.units == "" - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") -def test___analog_multi_channel_reader_with_extended_properties_flag___read_waveforms___only_includes_extended_properties( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - in_stream = ai_multi_channel_task_with_timing.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.EXTENDED_PROPERTIES - reader = AnalogMultiChannelReader(in_stream) - num_channels = ai_multi_channel_task_with_timing.number_of_channels - samples_to_read = 10 - waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 3 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan_index, waveform in enumerate(waveforms): - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(chan_index) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE - assert ( - waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name - ) - assert waveform.units == "Volts" - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") -def test___analog_multi_channel_reader_with_both_flags___read_waveforms___includes_both_timing_and_extended_properties( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - in_stream = ai_multi_channel_task_with_timing.in_stream - in_stream.waveform_attribute_mode = ( - WaveformAttributeMode.TIMING | WaveformAttributeMode.EXTENDED_PROPERTIES - ) - reader = AnalogMultiChannelReader(in_stream) - num_channels = ai_multi_channel_task_with_timing.number_of_channels - samples_to_read = 10 - waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 3 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan_index, waveform in enumerate(waveforms): - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(chan_index) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert isinstance(waveform.timing.timestamp, ht_datetime) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert ( - waveform.channel_name == ai_multi_channel_task_with_timing.ai_channels[chan_index].name - ) - assert waveform.units == "Volts" - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_analog_waveforms not implemented in GRPC") -def test___analog_multi_channel_reader_with_none_flag___read_waveforms___minimal_waveform_data( - ai_multi_channel_task_with_timing: nidaqmx.Task, -) -> None: - in_stream = ai_multi_channel_task_with_timing.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.NONE - reader = AnalogMultiChannelReader(in_stream) - num_channels = ai_multi_channel_task_with_timing.number_of_channels - samples_to_read = 10 - waveforms = [AnalogWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 3 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan_index, waveform in enumerate(waveforms): - assert isinstance(waveform, AnalogWaveform) - expected = _get_voltage_offset_for_chan(chan_index) - assert waveform.scaled_data == pytest.approx(expected, abs=VOLTAGE_EPSILON) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE - assert waveform.channel_name == "" - assert waveform.units == "" - assert waveform.sample_count == samples_to_read - - -def test___analog_unscaled_reader___read_int16___returns_valid_samples( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.int16).min, dtype=numpy.int16 - ) - - samples_read = reader.read_int16(data, number_of_samples_per_channel=samples_to_read) - - assert samples_read == samples_to_read - expected_vals = [ - _get_voltage_code_offset_for_chan(chan_index) for chan_index in range(num_channels) - ] - assert data == pytest.approx(expected_vals, abs=VOLTAGE_CODE_EPSILON) - - -def test___analog_unscaled_reader___read_int16___raises_error_with_correct_dtype( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_int16(data, number_of_samples_per_channel=samples_to_read) - - assert "int16" in exc_info.value.args[0] - - -def test___analog_unscaled_reader___read_uint16___returns_valid_samples( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16 - ) - - samples_read = reader.read_uint16(data, number_of_samples_per_channel=samples_to_read) - - assert samples_read == samples_to_read - expected_vals = [ - _get_voltage_code_offset_for_chan(chan_index) for chan_index in range(num_channels) - ] - # Promote to larger signed type to avoid overflow w/NumPy 2.0+. - assert data.astype(numpy.int32) == pytest.approx(expected_vals, abs=VOLTAGE_CODE_EPSILON) - - -def test___analog_unscaled_reader___read_uint16___raises_error_with_correct_dtype( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_uint16(data, number_of_samples_per_channel=samples_to_read) - - assert "uint16" in exc_info.value.args[0] - - -def test___analog_unscaled_reader___read_int32___returns_valid_samples( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.int32).min, dtype=numpy.int32 - ) - - samples_read = reader.read_int32(data, number_of_samples_per_channel=samples_to_read) - - assert samples_read == samples_to_read - expected_vals = [ - _get_voltage_code_offset_for_chan(chan_index) for chan_index in range(num_channels) - ] - assert data == pytest.approx(expected_vals, abs=VOLTAGE_CODE_EPSILON) - - -def test___analog_unscaled_reader___read_int32___raises_error_with_correct_dtype( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_int32(data, number_of_samples_per_channel=samples_to_read) - - assert "int32" in exc_info.value.args[0] - - -def test___analog_unscaled_reader___read_uint32___returns_valid_samples( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32 - ) - - samples_read = reader.read_uint32(data, number_of_samples_per_channel=samples_to_read) - - assert samples_read == samples_to_read - expected_vals = [ - _get_voltage_code_offset_for_chan(chan_index) for chan_index in range(num_channels) - ] - # Promote to larger signed type to avoid overflow w/NumPy 2.0+. - assert data.astype(numpy.int64) == pytest.approx(expected_vals, abs=VOLTAGE_CODE_EPSILON) - - -def test___analog_unscaled_reader___read_uint32___raises_error_with_correct_dtype( - ai_multi_channel_task: nidaqmx.Task, -) -> None: - reader = AnalogUnscaledReader(ai_multi_channel_task.in_stream) - num_channels = ai_multi_channel_task.number_of_channels - samples_to_read = 10 - data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_uint32(data, number_of_samples_per_channel=samples_to_read) - - assert "uint32" in exc_info.value.args[0] - - -POWER_EPSILON = 1e-3 -POWER_BINARY_EPSILON = 1 - - -def _get_voltage_setpoint_for_chan(chan_index: int) -> float: - return float(chan_index + 1) - - -def _get_current_setpoint_for_chan(chan_index: int) -> float: - return float(chan_index + 1) - - -def _pwr_volts_to_codes(volts: float, codes_per_volt: int = 4096) -> int: - return int(volts * codes_per_volt) - - -def _pwr_current_to_codes(current: float, codes_per_amp: int = 8192) -> int: - return int(current * codes_per_amp) - - -def _get_voltage_code_setpoint_for_chan(chan_index: int) -> int: - return _pwr_volts_to_codes(_get_voltage_setpoint_for_chan(chan_index)) - - -def _get_current_code_setpoint_for_chan(chan_index: int) -> int: - return _pwr_current_to_codes(_get_current_setpoint_for_chan(chan_index)) - - -@pytest.fixture -def pwr_single_channel_task( - task: nidaqmx.Task, sim_ts_power_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.ai_channels.add_ai_power_chan( - f"{sim_ts_power_device.name}/power", - _get_voltage_setpoint_for_chan(0), - _get_current_setpoint_for_chan(0), - True, # output enable - ) - return task - - -@pytest.fixture -def pwr_multi_channel_task( - task: nidaqmx.Task, sim_ts_power_devices: list[nidaqmx.system.Device] -) -> nidaqmx.Task: - for chan_index, sim_ts_power_device in enumerate(sim_ts_power_devices): - task.ai_channels.add_ai_power_chan( - f"{sim_ts_power_device.name}/power", - _get_voltage_setpoint_for_chan(chan_index), - _get_current_setpoint_for_chan(chan_index), - True, # output enable - ) - return task - - -def test___power_single_channel_reader___read_one_sample___returns_valid_samples( - pwr_single_channel_task: nidaqmx.Task, -) -> None: - reader = PowerSingleChannelReader(pwr_single_channel_task.in_stream) - - data = reader.read_one_sample() - - assert data.voltage == pytest.approx(_get_voltage_setpoint_for_chan(0), abs=POWER_EPSILON) - assert data.current == pytest.approx(_get_current_setpoint_for_chan(0), abs=POWER_EPSILON) - - -def test___power_single_channel_reader___read_many_sample___returns_valid_samples( - pwr_single_channel_task: nidaqmx.Task, -) -> None: - reader = PowerSingleChannelReader(pwr_single_channel_task.in_stream) - samples_to_read = 10 - voltage_data = numpy.full(samples_to_read, math.inf, dtype=numpy.float64) - current_data = numpy.full(samples_to_read, math.inf, dtype=numpy.float64) - - samples_read = reader.read_many_sample(voltage_data, current_data, samples_to_read) - - assert samples_read == samples_to_read - assert voltage_data == pytest.approx(_get_voltage_setpoint_for_chan(0), abs=POWER_EPSILON) - assert current_data == pytest.approx(_get_current_setpoint_for_chan(0), abs=POWER_EPSILON) - - -@pytest.mark.parametrize( - "voltage_dtype, current_dtype", - [ - (numpy.float32, numpy.float64), - (numpy.float64, numpy.float32), - (numpy.float32, numpy.float32), - ], -) -def test___power_single_channel_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( - pwr_single_channel_task: nidaqmx.Task, - voltage_dtype: numpy.typing.DTypeLike, - current_dtype: numpy.typing.DTypeLike, -) -> None: - reader = PowerSingleChannelReader(pwr_single_channel_task.in_stream) - samples_to_read = 10 - voltage_data = numpy.full(samples_to_read, math.inf, dtype=voltage_dtype) - current_data = numpy.full(samples_to_read, math.inf, dtype=current_dtype) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample(voltage_data, current_data, samples_to_read) - - assert "float64" in exc_info.value.args[0] - - -def test___power_multi_channel_reader___read_one_sample___returns_valid_samples( - pwr_multi_channel_task: nidaqmx.Task, -) -> None: - reader = PowerMultiChannelReader(pwr_multi_channel_task.in_stream) - num_channels = pwr_multi_channel_task.number_of_channels - voltage_data = numpy.full(num_channels, math.inf, dtype=numpy.float64) - current_data = numpy.full(num_channels, math.inf, dtype=numpy.float64) - - reader.read_one_sample(voltage_data, current_data) - - assert voltage_data == pytest.approx( - [_get_voltage_setpoint_for_chan(chan_index) for chan_index in range(num_channels)], - abs=POWER_EPSILON, - ) - assert current_data == pytest.approx( - [_get_current_setpoint_for_chan(chan_index) for chan_index in range(num_channels)], - abs=POWER_EPSILON, - ) - - -@pytest.mark.parametrize( - "voltage_dtype, current_dtype", - [ - (numpy.float32, numpy.float64), - (numpy.float64, numpy.float32), - (numpy.float32, numpy.float32), - ], -) -def test___power_multi_channel_reader___read_one_sample_with_wrong_dtype___raises_error_with_correct_dtype( - pwr_multi_channel_task: nidaqmx.Task, - voltage_dtype: numpy.typing.DTypeLike, - current_dtype: numpy.typing.DTypeLike, -) -> None: - reader = PowerMultiChannelReader(pwr_multi_channel_task.in_stream) - num_channels = pwr_multi_channel_task.number_of_channels - voltage_data = numpy.full(num_channels, math.inf, dtype=voltage_dtype) - current_data = numpy.full(num_channels, math.inf, dtype=current_dtype) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - reader.read_one_sample(voltage_data, current_data) - - assert "float64" in exc_info.value.args[0] - - -def test___power_multi_channel_reader___read_many_sample___returns_valid_samples( - pwr_multi_channel_task: nidaqmx.Task, -) -> None: - reader = PowerMultiChannelReader(pwr_multi_channel_task.in_stream) - num_channels = pwr_multi_channel_task.number_of_channels - samples_to_read = 10 - voltage_data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) - current_data = numpy.full((num_channels, samples_to_read), math.inf, dtype=numpy.float64) - - samples_read = reader.read_many_sample( - voltage_data, current_data, number_of_samples_per_channel=samples_to_read - ) - - assert samples_read == samples_to_read - expected_voltage_vals = [ - _get_voltage_setpoint_for_chan(chan_index) for chan_index in range(num_channels) - ] - expected_current_vals = [ - _get_current_setpoint_for_chan(chan_index) for chan_index in range(num_channels) - ] - assert voltage_data == pytest.approx(expected_voltage_vals, abs=POWER_EPSILON) - assert current_data == pytest.approx(expected_current_vals, abs=POWER_EPSILON) - - -@pytest.mark.parametrize( - "voltage_dtype, current_dtype", - [ - (numpy.float32, numpy.float64), - (numpy.float64, numpy.float32), - (numpy.float32, numpy.float32), - ], -) -def test___power_multi_channel_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( - pwr_multi_channel_task: nidaqmx.Task, - voltage_dtype: numpy.typing.DTypeLike, - current_dtype: numpy.typing.DTypeLike, -) -> None: - reader = PowerMultiChannelReader(pwr_multi_channel_task.in_stream) - num_channels = pwr_multi_channel_task.number_of_channels - samples_to_read = 10 - voltage_data = numpy.full((num_channels, samples_to_read), math.inf, dtype=voltage_dtype) - current_data = numpy.full((num_channels, samples_to_read), math.inf, dtype=current_dtype) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample( - voltage_data, current_data, number_of_samples_per_channel=samples_to_read - ) - - assert "float64" in exc_info.value.args[0] - - -def test___power_binary_reader___read_many_sample___returns_valid_samples( - pwr_multi_channel_task: nidaqmx.Task, -) -> None: - reader = PowerBinaryReader(pwr_multi_channel_task.in_stream) - num_channels = pwr_multi_channel_task.number_of_channels - samples_to_read = 10 - voltage_data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.int16).min, dtype=numpy.int16 - ) - current_data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.int16).min, dtype=numpy.int16 - ) - - samples_read = reader.read_many_sample( - voltage_data, current_data, number_of_samples_per_channel=samples_to_read - ) - - assert samples_read == samples_to_read - expected_voltage_vals = [ - _get_voltage_code_setpoint_for_chan(chan_index) for chan_index in range(num_channels) - ] - expected_current_vals = [ - _get_current_code_setpoint_for_chan(chan_index) for chan_index in range(num_channels) - ] - assert voltage_data == pytest.approx(expected_voltage_vals, abs=POWER_BINARY_EPSILON) - assert current_data == pytest.approx(expected_current_vals, abs=POWER_BINARY_EPSILON) - - -@pytest.mark.parametrize( - "voltage_dtype, voltage_default, current_dtype, current_default", - [ - (numpy.float64, math.inf, numpy.int16, numpy.iinfo(numpy.int16).min), - (numpy.int16, numpy.iinfo(numpy.int16).min, numpy.float64, math.inf), - (numpy.float64, math.inf, numpy.float64, math.inf), - ], -) -def test___power_binary_reader___read_many_sample_with_wrong_dtype___raises_error_with_correct_dtype( - pwr_multi_channel_task: nidaqmx.Task, - voltage_dtype: numpy.typing.DTypeLike, - voltage_default: float | int, - current_dtype: numpy.typing.DTypeLike, - current_default: float | int, -) -> None: - reader = PowerBinaryReader(pwr_multi_channel_task.in_stream) - num_channels = pwr_multi_channel_task.number_of_channels - samples_to_read = 10 - voltage_data = numpy.full((num_channels, samples_to_read), voltage_default, dtype=voltage_dtype) - current_data = numpy.full((num_channels, samples_to_read), current_default, dtype=current_dtype) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample( - voltage_data, current_data, number_of_samples_per_channel=samples_to_read - ) - - assert "int16" in exc_info.value.args[0] diff --git a/tests/component/test_stream_readers_di.py b/tests/component/test_stream_readers_di.py deleted file mode 100644 index c127ddc75..000000000 --- a/tests/component/test_stream_readers_di.py +++ /dev/null @@ -1,1532 +0,0 @@ -from __future__ import annotations - -import ctypes -import math -from datetime import timezone -from typing import Callable, TypeVar - -import numpy -import numpy.typing -import pytest -from hightime import datetime as ht_datetime, timedelta as ht_timedelta -from nitypes.waveform import DigitalWaveform, SampleIntervalMode - -import nidaqmx -import nidaqmx.system -from nidaqmx._feature_toggles import WAVEFORM_SUPPORT, FeatureNotSupportedError -from nidaqmx.constants import AcquisitionType, LineGrouping, WaveformAttributeMode -from nidaqmx.error_codes import DAQmxErrors -from nidaqmx.stream_readers import ( - DaqError, - DigitalMultiChannelReader, - DigitalSingleChannelReader, -) -from nidaqmx.utils import flatten_channel_string - - -@pytest.fixture -def di_single_line_task(task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device) -> nidaqmx.Task: - task.di_channels.add_di_chan( - sim_6363_device.di_lines[0].name, line_grouping=LineGrouping.CHAN_FOR_ALL_LINES - ) - return task - - -@pytest.fixture -def di_single_line_timing_task(di_single_line_task: nidaqmx.Task) -> nidaqmx.Task: - di_single_line_task.timing.cfg_samp_clk_timing( - rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return di_single_line_task - - -@pytest.fixture -def di_single_line_high_rate_task(di_single_line_task: nidaqmx.Task) -> nidaqmx.Task: - di_single_line_task.timing.cfg_samp_clk_timing( - rate=10_000_000, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return di_single_line_task - - -@pytest.fixture -def di_single_channel_multi_line_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[:8]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -@pytest.fixture -def di_single_channel_multi_line_timing_task( - di_single_channel_multi_line_task: nidaqmx.Task, -) -> nidaqmx.Task: - di_single_channel_multi_line_task.timing.cfg_samp_clk_timing( - rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return di_single_channel_multi_line_task - - -@pytest.fixture -def di_single_chan_lines_and_port_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - flatten_channel_string( - sim_6363_device.di_lines.channel_names[0:3] + [sim_6363_device.di_ports[1].name] - ), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -@pytest.fixture -def di_multi_channel_multi_line_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[:8]), - line_grouping=LineGrouping.CHAN_PER_LINE, - ) - return task - - -@pytest.fixture -def di_multi_chan_multi_line_timing_task( - di_multi_channel_multi_line_task: nidaqmx.Task, -) -> nidaqmx.Task: - di_multi_channel_multi_line_task.timing.cfg_samp_clk_timing( - rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return di_multi_channel_multi_line_task - - -@pytest.fixture -def di_multi_chan_diff_lines_timing_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[0:1]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[1:3]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[3:7]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.timing.cfg_samp_clk_timing( - rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return task - - -@pytest.fixture -def di_multi_chan_lines_and_port_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[0:1]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[1:3]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - flatten_channel_string(sim_6363_device.di_lines.channel_names[3:7]), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.di_channels.add_di_chan( - sim_6363_device.di_ports[1].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -@pytest.fixture -def di_single_channel_port_byte_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - # 6363 port 1 has 8 lines - task.di_channels.add_di_chan( - sim_6363_device.di_ports[1].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -@pytest.fixture -def di_multi_channel_port_byte_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - # 6363 port 1 has 8 lines - task.di_channels.add_di_chan( - sim_6363_device.di_ports[1].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - # 6363 port 2 has 8 lines - task.di_channels.add_di_chan( - sim_6363_device.di_ports[2].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -@pytest.fixture -def di_single_channel_port_uint16_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - # 6363 port 1 has 8 lines, and DAQ will happily extend the data to the larger type. - task.di_channels.add_di_chan( - sim_6363_device.di_ports[1].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -@pytest.fixture -def di_multi_channel_port_uint16_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - # 6363 port 1 has 8 lines, and DAQ will happily extend the data to the larger type. - task.di_channels.add_di_chan( - sim_6363_device.di_ports[1].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - # 6363 port 2 has 8 lines, and DAQ will happily extend the data to the larger type. - task.di_channels.add_di_chan( - sim_6363_device.di_ports[2].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -@pytest.fixture -def di_single_channel_port_uint32_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - # 6363 port 0 has 32 lines - task.di_channels.add_di_chan( - sim_6363_device.di_ports[0].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -@pytest.fixture -def di_single_channel_port_uint32_timing_task( - di_single_channel_port_uint32_task: nidaqmx.Task, -) -> nidaqmx.Task: - di_single_channel_port_uint32_task.timing.cfg_samp_clk_timing( - rate=1000.0, sample_mode=AcquisitionType.FINITE, samps_per_chan=50 - ) - return di_single_channel_port_uint32_task - - -@pytest.fixture -def di_multi_channel_port_uint32_task( - task: nidaqmx.Task, sim_6363_device: nidaqmx.system.Device -) -> nidaqmx.Task: - # 6363 port 0 has 32 lines - task.di_channels.add_di_chan( - sim_6363_device.di_ports[0].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - # 6363 port 1 has 8 lines, and DAQ will happily extend the data to the larger type. - task.di_channels.add_di_chan( - sim_6363_device.di_ports[1].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - # 6363 port 2 has 8 lines, and DAQ will happily extend the data to the larger type. - task.di_channels.add_di_chan( - sim_6363_device.di_ports[2].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - return task - - -def _get_num_lines_in_task(task: nidaqmx.Task) -> int: - return sum([chan.di_num_lines for chan in task.channels]) - - -def _get_expected_digital_data_for_sample(num_lines: int, sample_number: int) -> int: - result = 0 - # Simulated digital signals "count" from 0 in binary within each group of 8 lines. - for _ in range((num_lines + 7) // 8): - result = (result << 8) | sample_number - - line_mask = (2**num_lines) - 1 - return result & line_mask - - -def _get_expected_data_for_line(num_samples: int, line_number: int) -> list[int]: - data = [] - # Simulated digital signals "count" from 0 in binary within each group of 8 lines. - # Each line represents a bit in the binary representation of the sample number. - # - line 0 represents bit 0 (LSB) - alternates every sample: 0,1,0,1,0,1,0,1... - # - line 1 represents bit 1 - alternates every 2 samples: 0,0,1,1,0,0,1,1... - # - line 2 represents bit 2 - alternates every 4 samples: 0,0,0,0,1,1,1,1... - line_number %= 8 - for sample_num in range(num_samples): - bit_value = (sample_num >> line_number) & 1 - data.append(bit_value) - return data - - -def _get_expected_digital_data(num_lines: int, num_samples: int) -> list[int]: - return [ - _get_expected_digital_data_for_sample(num_lines, sample_number) - for sample_number in range(num_samples) - ] - - -def _get_expected_digital_port_data_port_major( - task: nidaqmx.Task, num_samples: int -) -> list[list[int]]: - return [_get_expected_digital_data(chan.di_num_lines, num_samples) for chan in task.channels] - - -def _get_expected_digital_port_data_sample_major( - task: nidaqmx.Task, num_samples: int -) -> list[list[int]]: - result = _get_expected_digital_port_data_port_major(task, num_samples) - return numpy.transpose(result).tolist() - - -def _bool_array_to_int(bool_array: numpy.typing.NDArray[numpy.bool_]) -> int: - result = 0 - # Simulated data is little-endian - for bit in bool_array[::-1]: - result = (result << 1) | int(bit) - return result - - -def _get_waveform_data(waveform: DigitalWaveform) -> list[int]: - assert isinstance(waveform, DigitalWaveform) - return [_bool_array_to_int(sample) for sample in waveform.data] - - -def _is_timestamp_close_to_now(timestamp: ht_datetime, tolerance_seconds: float = 1.0) -> bool: - assert isinstance(timestamp, ht_datetime) - current_time = ht_datetime.now(timezone.utc) - time_diff = abs((timestamp - current_time).total_seconds()) - return time_diff <= tolerance_seconds - - -_D = TypeVar("_D", bound=numpy.generic) - - -def _read_and_copy( - read_func: Callable[[numpy.typing.NDArray[_D]], None], array: numpy.typing.NDArray[_D] -) -> numpy.typing.NDArray[_D]: - read_func(array) - return array.copy() - - -def test___digital_single_channel_reader___read_one_sample_one_line___returns_valid_samples( - di_single_line_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_line_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_line_task) - samples_to_read = 256 - - data = [reader.read_one_sample_one_line() for _ in range(samples_to_read)] - - assert data == _get_expected_digital_data(num_lines, samples_to_read) - - -def test___digital_multi_channel_reader___read_one_sample_one_line___returns_valid_samples( - di_single_line_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_single_line_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_line_task) - samples_to_read = 256 - sample = numpy.full(num_lines, False, dtype=numpy.bool_) - - data = [_read_and_copy(reader.read_one_sample_one_line, sample) for _ in range(samples_to_read)] - - assert [_bool_array_to_int(sample) for sample in data] == _get_expected_digital_data( - num_lines, samples_to_read - ) - - -def test___digital_multi_channel_reader___read_one_sample_one_line_with_wrong_dtype___raises_error_with_correct_dtype( - di_single_line_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_single_line_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_line_task) - data = numpy.full(num_lines, math.inf, dtype=numpy.float64) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - reader.read_one_sample_one_line(data) - - assert "bool" in exc_info.value.args[0] - - -def test___digital_single_channel_reader___read_one_sample_multi_line___returns_valid_samples( - di_single_channel_multi_line_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_multi_line_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_multi_line_task) - samples_to_read = 256 - sample = numpy.full(num_lines, False, dtype=numpy.bool_) - - data = [ - _read_and_copy(reader.read_one_sample_multi_line, sample) for _ in range(samples_to_read) - ] - - assert [_bool_array_to_int(sample) for sample in data] == _get_expected_digital_data( - num_lines, samples_to_read - ) - - -def test___digital_single_channel_reader___read_one_sample_multi_line_with_wrong_dtype___raises_error_with_correct_dtype( - di_single_channel_multi_line_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_multi_line_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_multi_line_task) - data = numpy.full(num_lines, math.inf, dtype=numpy.float64) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - reader.read_one_sample_multi_line(data) - - assert "bool" in exc_info.value.args[0] - - -def test___digital_single_channel_reader___read_one_sample_port_byte___returns_valid_samples( - di_single_channel_port_byte_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_byte_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_port_byte_task) - samples_to_read = 256 - - data = [reader.read_one_sample_port_byte() for _ in range(samples_to_read)] - - assert data == _get_expected_digital_data(num_lines, samples_to_read) - - -def test___digital_single_channel_reader___read_one_sample_port_uint16___returns_valid_samples( - di_single_channel_port_uint16_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_uint16_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_port_uint16_task) - samples_to_read = 256 - - data = [reader.read_one_sample_port_uint16() for _ in range(samples_to_read)] - - assert data == _get_expected_digital_data(num_lines, samples_to_read) - - -def test___digital_single_channel_reader___read_one_sample_port_uint32___returns_valid_samples( - di_single_channel_port_uint32_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_uint32_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_port_uint32_task) - samples_to_read = 256 - - data = [reader.read_one_sample_port_uint32() for _ in range(samples_to_read)] - - assert data == _get_expected_digital_data(num_lines, samples_to_read) - - -def test___digital_single_channel_reader___read_many_sample_port_byte___returns_valid_samples( - di_single_channel_port_byte_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_byte_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_port_byte_task) - samples_to_read = 256 - data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8) - - samples_read = reader.read_many_sample_port_byte( - data, number_of_samples_per_channel=samples_to_read - ) - - assert samples_read == samples_to_read - assert data.tolist() == _get_expected_digital_data(num_lines, samples_to_read) - - -def test___digital_single_channel_reader___read_many_sample_port_byte_with_wrong_dtype___raises_error_with_correct_dtype( - di_single_channel_port_byte_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_byte_task.in_stream) - samples_to_read = 256 - data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample_port_byte(data, number_of_samples_per_channel=samples_to_read) - - assert "uint8" in exc_info.value.args[0] - - -def test___digital_single_channel_reader___read_many_sample_port_uint16___returns_valid_samples( - di_single_channel_port_uint16_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_uint16_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_port_uint16_task) - samples_to_read = 256 - data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16) - - samples_read = reader.read_many_sample_port_uint16( - data, number_of_samples_per_channel=samples_to_read - ) - - assert samples_read == samples_to_read - assert data.tolist() == _get_expected_digital_data(num_lines, samples_to_read) - - -def test___digital_single_channel_reader___read_many_sample_port_uint16_with_wrong_dtype___raises_error_with_correct_dtype( - di_single_channel_port_uint16_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_uint16_task.in_stream) - samples_to_read = 256 - data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample_port_uint16(data, number_of_samples_per_channel=samples_to_read) - - assert "uint16" in exc_info.value.args[0] - - -def test___digital_single_channel_reader___read_many_sample_port_uint32___returns_valid_samples( - di_single_channel_port_uint32_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_uint32_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_port_uint32_task) - samples_to_read = 256 - data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32) - - samples_read = reader.read_many_sample_port_uint32( - data, number_of_samples_per_channel=samples_to_read - ) - - assert samples_read == samples_to_read - assert data.tolist() == _get_expected_digital_data(num_lines, samples_to_read) - - -def test___digital_single_channel_reader___read_many_sample_port_uint32_with_wrong_dtype___raises_error_with_correct_dtype( - di_single_channel_port_uint32_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_uint32_task.in_stream) - samples_to_read = 256 - data = numpy.full(samples_to_read, numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample_port_uint32(data, number_of_samples_per_channel=samples_to_read) - - assert "uint32" in exc_info.value.args[0] - - -def test___digital_multi_channel_reader___read_one_sample_multi_line___returns_valid_samples( - di_multi_channel_multi_line_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_multi_line_task.in_stream) - num_channels = di_multi_channel_multi_line_task.number_of_channels - samples_to_read = 256 - sample = numpy.full((num_channels, 1), False, dtype=numpy.bool_) - - data = [ - _read_and_copy(reader.read_one_sample_multi_line, sample) for _ in range(samples_to_read) - ] - - assert [_bool_array_to_int(sample[:, 0]) for sample in data] == _get_expected_digital_data( - num_channels, samples_to_read - ) - - -def test___digital_multi_channel_reader___read_one_sample_multi_line_jagged___returns_valid_samples( - di_multi_channel_port_uint32_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) - num_channels = di_multi_channel_port_uint32_task.number_of_channels - samples_to_read = 256 - sample = numpy.full((num_channels, 32), False, dtype=numpy.bool_) - - data = [ - _read_and_copy(reader.read_one_sample_multi_line, sample) for _ in range(samples_to_read) - ] - - assert [ - [_bool_array_to_int(sample[chan, :]) for chan in range(num_channels)] for sample in data - ] == _get_expected_digital_port_data_sample_major( - di_multi_channel_port_uint32_task, samples_to_read - ) - - -def test___digital_multi_channel_reader___read_one_sample_multi_line_with_wrong_dtype___raises_error_with_correct_dtype( - di_multi_channel_multi_line_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_multi_line_task.in_stream) - num_channels = di_multi_channel_multi_line_task.number_of_channels - data = numpy.full((num_channels, 1), math.inf, dtype=numpy.float64) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - reader.read_one_sample_multi_line(data) - - assert "bool" in exc_info.value.args[0] - - -def test___digital_multi_channel_reader___read_one_sample_port_byte___returns_valid_samples( - di_multi_channel_port_byte_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_byte_task.in_stream) - num_channels = di_multi_channel_port_byte_task.number_of_channels - samples_to_read = 256 - sample = numpy.full(num_channels, numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8) - - data = [ - _read_and_copy(reader.read_one_sample_port_byte, sample).tolist() - for _ in range(samples_to_read) - ] - - assert data == _get_expected_digital_port_data_sample_major( - di_multi_channel_port_byte_task, samples_to_read - ) - - -def test___digital_multi_channel_reader___read_one_sample_port_byte_with_wrong_dtype___raises_error_with_correct_dtype( - di_multi_channel_port_byte_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_byte_task.in_stream) - num_channels = di_multi_channel_port_byte_task.number_of_channels - data = numpy.full(num_channels, numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - reader.read_one_sample_port_byte(data) - - assert "uint8" in exc_info.value.args[0] - - -def test___digital_multi_channel_reader___read_many_sample_port_byte___returns_valid_samples( - di_multi_channel_port_byte_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_byte_task.in_stream) - num_channels = di_multi_channel_port_byte_task.number_of_channels - samples_to_read = 256 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8 - ) - - samples_read = reader.read_many_sample_port_byte( - data, number_of_samples_per_channel=samples_to_read - ) - - assert samples_read == samples_to_read - assert data.tolist() == _get_expected_digital_port_data_port_major( - di_multi_channel_port_byte_task, samples_to_read - ) - - -def test___digital_multi_channel_reader___read_many_sample_port_byte_with_wrong_dtype___raises_error_with_correct_dtype( - di_multi_channel_port_byte_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_byte_task.in_stream) - num_channels = di_multi_channel_port_byte_task.number_of_channels - samples_to_read = 256 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16 - ) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample_port_byte(data, number_of_samples_per_channel=samples_to_read) - - assert "uint8" in exc_info.value.args[0] - - -def test___digital_multi_channel_reader___read_one_sample_port_uint16___returns_valid_samples( - di_multi_channel_port_uint16_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_uint16_task.in_stream) - num_channels = di_multi_channel_port_uint16_task.number_of_channels - samples_to_read = 256 - sample = numpy.full(num_channels, numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16) - - data = [ - _read_and_copy(reader.read_one_sample_port_uint16, sample).tolist() - for _ in range(samples_to_read) - ] - - assert data == _get_expected_digital_port_data_sample_major( - di_multi_channel_port_uint16_task, samples_to_read - ) - - -def test___digital_multi_channel_reader___read_one_sample_port_uint16_with_wrong_dtype___raises_error_with_correct_dtype( - di_multi_channel_port_uint16_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_uint16_task.in_stream) - num_channels = di_multi_channel_port_uint16_task.number_of_channels - data = numpy.full(num_channels, numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - reader.read_one_sample_port_uint16(data) - - assert "uint16" in exc_info.value.args[0] - - -def test___digital_multi_channel_reader___read_many_sample_port_uint16___returns_valid_samples( - di_multi_channel_port_uint16_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_uint16_task.in_stream) - num_channels = di_multi_channel_port_uint16_task.number_of_channels - samples_to_read = 256 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.uint16).min, dtype=numpy.uint16 - ) - - samples_read = reader.read_many_sample_port_uint16( - data, number_of_samples_per_channel=samples_to_read - ) - - assert samples_read == samples_to_read - assert data.tolist() == _get_expected_digital_port_data_port_major( - di_multi_channel_port_uint16_task, samples_to_read - ) - - -def test___digital_multi_channel_reader___read_many_sample_port_uint16_with_wrong_dtype___raises_error_with_correct_dtype( - di_multi_channel_port_uint16_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_uint16_task.in_stream) - num_channels = di_multi_channel_port_uint16_task.number_of_channels - samples_to_read = 256 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32 - ) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample_port_uint16(data, number_of_samples_per_channel=samples_to_read) - - assert "uint16" in exc_info.value.args[0] - - -def test___digital_multi_channel_reader___read_one_sample_port_uint32___returns_valid_samples( - di_multi_channel_port_uint32_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) - num_channels = di_multi_channel_port_uint32_task.number_of_channels - samples_to_read = 256 - sample = numpy.full(num_channels, numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32) - - data = [ - _read_and_copy(reader.read_one_sample_port_uint32, sample).tolist() - for _ in range(samples_to_read) - ] - - assert data == _get_expected_digital_port_data_sample_major( - di_multi_channel_port_uint32_task, samples_to_read - ) - - -def test___digital_multi_channel_reader___read_one_sample_port_uint32_with_wrong_dtype___raises_error_with_correct_dtype( - di_multi_channel_port_uint32_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) - num_channels = di_multi_channel_port_uint32_task.number_of_channels - data = numpy.full(num_channels, numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - reader.read_one_sample_port_uint32(data) - - assert "uint32" in exc_info.value.args[0] - - -def test___digital_multi_channel_reader___read_many_sample_port_uint32___returns_valid_samples( - di_multi_channel_port_uint32_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) - num_channels = di_multi_channel_port_uint32_task.number_of_channels - samples_to_read = 256 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.uint32).min, dtype=numpy.uint32 - ) - - samples_read = reader.read_many_sample_port_uint32( - data, number_of_samples_per_channel=samples_to_read - ) - - assert samples_read == samples_to_read - assert data.tolist() == _get_expected_digital_port_data_port_major( - di_multi_channel_port_uint32_task, samples_to_read - ) - - -def test___digital_multi_channel_reader___read_many_sample_port_uint32_with_wrong_dtype___raises_error_with_correct_dtype( - di_multi_channel_port_uint32_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_channel_port_uint32_task.in_stream) - num_channels = di_multi_channel_port_uint32_task.number_of_channels - samples_to_read = 256 - data = numpy.full( - (num_channels, samples_to_read), numpy.iinfo(numpy.uint8).min, dtype=numpy.uint8 - ) - - with pytest.raises((ctypes.ArgumentError, TypeError)) as exc_info: - _ = reader.read_many_sample_port_uint32(data, number_of_samples_per_channel=samples_to_read) - - assert "uint32" in exc_info.value.args[0] - - -@pytest.mark.disable_feature_toggle(WAVEFORM_SUPPORT) -def test___digital_single_line_reader___read_waveform_feature_disabled___raises_feature_not_supported_error( - di_single_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) - waveform = DigitalWaveform(50) - - with pytest.raises(FeatureNotSupportedError) as exc_info: - reader.read_waveform(waveform) - - error_message = str(exc_info.value) - assert "WAVEFORM_SUPPORT feature is not supported" in error_message - assert "NIDAQMX_ENABLE_WAVEFORM_SUPPORT" in error_message - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader___read_waveform___returns_valid_waveform( - di_single_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) - samples_to_read = 10 - waveform = DigitalWaveform(samples_to_read) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert _get_waveform_data(waveform) == _get_expected_digital_data(1, samples_to_read) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_channel_multi_line_reader___read_waveform___returns_valid_waveform( - di_single_channel_multi_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_multi_line_timing_task.in_stream) - samples_to_read = 10 - num_lines = _get_num_lines_in_task(di_single_channel_multi_line_timing_task) - waveform = DigitalWaveform(samples_to_read, num_lines) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert _get_waveform_data(waveform) == _get_expected_digital_data(num_lines, samples_to_read) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.channel_name == di_single_channel_multi_line_timing_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader___read_waveform_no_args___returns_valid_waveform( - di_single_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) - waveform = DigitalWaveform(50) - - samples_read = reader.read_waveform(waveform) - - assert samples_read == 50 - assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_channel_multi_line_reader___read_waveform_no_args___returns_valid_waveform( - di_single_channel_multi_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_multi_line_timing_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_multi_line_timing_task) - waveform = DigitalWaveform(50, num_lines) - - samples_read = reader.read_waveform(waveform) - - assert samples_read == 50 - assert _get_waveform_data(waveform) == _get_expected_digital_data(num_lines, 50) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.channel_name == di_single_channel_multi_line_timing_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader___read_waveform_in_place___returns_valid_waveform( - di_single_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) - waveform = DigitalWaveform(50) - - samples_read = reader.read_waveform(waveform) - - assert samples_read == 50 - assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_channel_multi_line_reader___read_waveform_in_place___returns_valid_waveform( - di_single_channel_multi_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_multi_line_timing_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_multi_line_timing_task) - waveform = DigitalWaveform(sample_count=50, signal_count=8) - - samples_read = reader.read_waveform(waveform) - - assert samples_read == 50 - assert _get_waveform_data(waveform) == _get_expected_digital_data(num_lines, 50) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == di_single_channel_multi_line_timing_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader___reuse_waveform_in_place___overwrites_data_timing_and_attributes( - generate_task: Callable[[], nidaqmx.Task], sim_6363_device: nidaqmx.system.Device -) -> None: - def _make_single_line_reader(chan_index, rate): - task = generate_task() - task.di_channels.add_di_chan( - sim_6363_device.di_lines[chan_index].name, - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.timing.cfg_samp_clk_timing(rate, sample_mode=AcquisitionType.FINITE, samps_per_chan=10) - return DigitalSingleChannelReader(task.in_stream) - - sample_count = 10 - reader0 = _make_single_line_reader(chan_index=0, rate=1000.0) - reader1 = _make_single_line_reader(chan_index=1, rate=2000.0) - waveform = DigitalWaveform(sample_count) - - reader0.read_waveform(waveform, sample_count) - timestamp0 = waveform.timing.timestamp - assert _get_waveform_data(waveform) == _get_expected_data_for_line(sample_count, 0) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == sim_6363_device.di_lines[0].name - - reader1.read_waveform(waveform, sample_count) - timestamp1 = waveform.timing.timestamp - assert _get_waveform_data(waveform) == _get_expected_data_for_line(sample_count, 1) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 2000) - assert waveform.channel_name == sim_6363_device.di_lines[1].name - - assert timestamp1 > timestamp0 - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_channel_multi_line_reader___reuse_waveform_in_place___overwrites_data_timing_and_attributes( - generate_task: Callable[[], nidaqmx.Task], sim_6363_device: nidaqmx.system.Device -) -> None: - def _make_single_channel_multi_line_reader(lines_start, rate): - task = generate_task() - task.di_channels.add_di_chan( - flatten_channel_string( - sim_6363_device.di_lines.channel_names[lines_start : lines_start + 4] - ), - line_grouping=LineGrouping.CHAN_FOR_ALL_LINES, - ) - task.timing.cfg_samp_clk_timing(rate, sample_mode=AcquisitionType.FINITE, samps_per_chan=10) - return DigitalSingleChannelReader(task.in_stream) - - sample_count = 10 - signal_count = 4 - reader0 = _make_single_channel_multi_line_reader(lines_start=0, rate=1000.0) - reader1 = _make_single_channel_multi_line_reader(lines_start=1, rate=2000.0) - waveform = DigitalWaveform(sample_count, signal_count) - - reader0.read_waveform(waveform, sample_count) - timestamp0 = waveform.timing.timestamp - assert _get_waveform_data(waveform) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == f"{sim_6363_device.di_lines[0].name}..." - - reader1.read_waveform(waveform, sample_count) - timestamp1 = waveform.timing.timestamp - assert _get_waveform_data(waveform) == [0, 0, 1, 1, 2, 2, 3, 3, 4, 4] - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 2000) - assert waveform.channel_name == f"{sim_6363_device.di_lines[1].name}..." - - assert timestamp1 > timestamp0 - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader___read_into_undersized_waveform___throws_exception( - di_single_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_line_timing_task.in_stream) - samples_to_read = 10 - - waveform = DigitalWaveform(samples_to_read - 1) - with pytest.raises(DaqError) as exc_info: - reader.read_waveform(waveform, samples_to_read) - - assert exc_info.value.error_code == DAQmxErrors.READ_BUFFER_TOO_SMALL - assert exc_info.value.args[0].startswith("Buffer is too small to fit read data.") - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader___read_waveform_high_sample_rate___returns_correct_sample_interval( - di_single_line_high_rate_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_line_high_rate_task.in_stream) - samples_to_read = 50 - waveform = DigitalWaveform(samples_to_read) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 10_000_000) - assert waveform.sample_count == samples_to_read - assert waveform.channel_name == di_single_line_high_rate_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader_with_timing_flag___read_waveform___only_includes_timing_data( - di_single_line_timing_task: nidaqmx.Task, -) -> None: - in_stream = di_single_line_timing_task.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.TIMING - reader = DigitalSingleChannelReader(in_stream) - waveform = DigitalWaveform(50) - - samples_read = reader.read_waveform(waveform) - - assert samples_read == 50 - assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == "" - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader_with_extended_properties_flag___read_waveform___only_includes_extended_properties( - di_single_line_timing_task: nidaqmx.Task, -) -> None: - in_stream = di_single_line_timing_task.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.EXTENDED_PROPERTIES - reader = DigitalSingleChannelReader(in_stream) - waveform = DigitalWaveform(50) - - samples_read = reader.read_waveform(waveform) - - assert samples_read == 50 - assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE - assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader_with_both_flags___read_waveform___includes_both_timing_and_extended_properties( - di_single_line_timing_task: nidaqmx.Task, -) -> None: - in_stream = di_single_line_timing_task.in_stream - in_stream.waveform_attribute_mode = ( - WaveformAttributeMode.TIMING | WaveformAttributeMode.EXTENDED_PROPERTIES - ) - reader = DigitalSingleChannelReader(in_stream) - waveform = DigitalWaveform(50) - - samples_read = reader.read_waveform(waveform) - - assert samples_read == 50 - assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == di_single_line_timing_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_line_reader_with_none_flag___read_waveform___minimal_waveform_data( - di_single_line_timing_task: nidaqmx.Task, -) -> None: - in_stream = di_single_line_timing_task.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.NONE - reader = DigitalSingleChannelReader(in_stream) - waveform = DigitalWaveform(50) - - samples_read = reader.read_waveform(waveform) - - assert samples_read == 50 - assert _get_waveform_data(waveform) == _get_expected_digital_data(1, 50) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE - assert waveform.channel_name == "" - - -@pytest.mark.xfail( - reason="TODO: AB#3178052 - DigitalWaveform signal index is reversed when channels are specified by ports", - raises=AssertionError, -) -@pytest.mark.grpc_skip(reason="read_digital_waveform not implemented in GRPC") -def test___digital_single_channel_port_uint32_reader___read_waveform___returns_valid_waveform( - di_single_channel_port_uint32_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalSingleChannelReader(di_single_channel_port_uint32_timing_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_channel_port_uint32_timing_task) - samples_to_read = 10 - waveform = DigitalWaveform(samples_to_read, num_lines) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == 50 - assert _get_waveform_data(waveform) == _get_expected_digital_data(num_lines, samples_to_read) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.channel_name == di_single_channel_port_uint32_timing_task.di_channels[0].name - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_single_channel_lines_and_port___read_waveform___returns_valid_waveform( - di_single_chan_lines_and_port_task: nidaqmx.Task, - sim_6363_device: nidaqmx.system.Device, -) -> None: - reader = DigitalSingleChannelReader(di_single_chan_lines_and_port_task.in_stream) - num_lines = _get_num_lines_in_task(di_single_chan_lines_and_port_task) - samples_to_read = 10 - waveform = DigitalWaveform(samples_to_read, num_lines) - - samples_read = reader.read_waveform(waveform, samples_to_read) - - assert samples_read == samples_to_read - # Note, the data on the port's waveform is MSB instead of LSB because of bug AB#3178052 - # When that bug is fixed, these asserts should be updated - assert _get_waveform_data(waveform) == [0, 1025, 514, 1539, 260, 1285, 774, 1799, 128, 1153] - assert waveform.sample_count == samples_to_read - assert waveform.channel_name == di_single_chan_lines_and_port_task.di_channels[0].name - assert waveform._get_signal_names() == [ - sim_6363_device.di_lines[0].name, - sim_6363_device.di_lines[1].name, - sim_6363_device.di_lines[2].name, - sim_6363_device.di_lines[39].name, - sim_6363_device.di_lines[38].name, - sim_6363_device.di_lines[37].name, - sim_6363_device.di_lines[36].name, - sim_6363_device.di_lines[35].name, - sim_6363_device.di_lines[34].name, - sim_6363_device.di_lines[33].name, - sim_6363_device.di_lines[32].name, - ] - - -@pytest.mark.disable_feature_toggle(WAVEFORM_SUPPORT) -def test___digital_multi_channel_multi_line_reader___read_waveforms_feature_disabled___raises_feature_not_supported_error( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) - waveforms = [DigitalWaveform(50) for _ in range(8)] - - with pytest.raises(FeatureNotSupportedError) as exc_info: - reader.read_waveforms(waveforms) - - error_message = str(exc_info.value) - assert "WAVEFORM_SUPPORT feature is not supported" in error_message - assert "NIDAQMX_ENABLE_WAVEFORM_SUPPORT" in error_message - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader___read_waveforms___returns_valid_waveforms( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) - num_channels = di_multi_chan_multi_line_timing_task.number_of_channels - num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) - samples_to_read = 10 - waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 8 - assert num_lines == 8 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan, waveform in enumerate(waveforms): - assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_different_lines_reader___read_waveforms___returns_valid_waveforms( - di_multi_chan_diff_lines_timing_task: nidaqmx.Task, - sim_6363_device: nidaqmx.system.Device, -) -> None: - reader = DigitalMultiChannelReader(di_multi_chan_diff_lines_timing_task.in_stream) - num_channels = di_multi_chan_diff_lines_timing_task.number_of_channels - num_lines = _get_num_lines_in_task(di_multi_chan_diff_lines_timing_task) - samples_to_read = 10 - waveforms = [ - DigitalWaveform(samples_to_read, 1), - DigitalWaveform(samples_to_read, 2), - DigitalWaveform(samples_to_read, 4), - ] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 3 - assert num_lines == 7 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - assert _get_waveform_data(waveforms[0]) == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] - assert _is_timestamp_close_to_now(waveforms[0].timing.timestamp) - assert waveforms[0].sample_count == samples_to_read - assert waveforms[0].timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveforms[0].channel_name == di_multi_chan_diff_lines_timing_task.di_channels[0].name - assert waveforms[0]._get_signal_names() == [ - sim_6363_device.di_lines[0].name, - ] - assert _get_waveform_data(waveforms[1]) == [0, 0, 1, 1, 2, 2, 3, 3, 0, 0] - assert _is_timestamp_close_to_now(waveforms[1].timing.timestamp) - assert waveforms[1].sample_count == samples_to_read - assert waveforms[1].timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveforms[1].channel_name == di_multi_chan_diff_lines_timing_task.di_channels[1].name - assert waveforms[1]._get_signal_names() == [ - sim_6363_device.di_lines[1].name, - sim_6363_device.di_lines[2].name, - ] - assert _get_waveform_data(waveforms[2]) == [0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - assert _is_timestamp_close_to_now(waveforms[2].timing.timestamp) - assert waveforms[2].sample_count == samples_to_read - assert waveforms[2].timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveforms[2].channel_name == di_multi_chan_diff_lines_timing_task.di_channels[2].name - assert waveforms[2]._get_signal_names() == [ - sim_6363_device.di_lines[3].name, - sim_6363_device.di_lines[4].name, - sim_6363_device.di_lines[5].name, - sim_6363_device.di_lines[6].name, - ] - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_lines_and_port_reader___read_waveforms___returns_valid_waveforms( - di_multi_chan_lines_and_port_task: nidaqmx.Task, - sim_6363_device: nidaqmx.system.Device, -) -> None: - reader = DigitalMultiChannelReader(di_multi_chan_lines_and_port_task.in_stream) - num_channels = di_multi_chan_lines_and_port_task.number_of_channels - num_lines = _get_num_lines_in_task(di_multi_chan_lines_and_port_task) - samples_to_read = 10 - waveforms = [ - DigitalWaveform(samples_to_read, 1), - DigitalWaveform(samples_to_read, 2), - DigitalWaveform(samples_to_read, 4), - DigitalWaveform(samples_to_read, 8), - ] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 4 - assert num_lines == 15 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - assert _get_waveform_data(waveforms[0]) == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] - assert _is_timestamp_close_to_now(waveforms[0].timing.timestamp) - assert waveforms[0].sample_count == samples_to_read - assert waveforms[0].channel_name == di_multi_chan_lines_and_port_task.di_channels[0].name - assert waveforms[0]._get_signal_names() == [ - sim_6363_device.di_lines[0].name, - ] - assert _get_waveform_data(waveforms[1]) == [0, 0, 1, 1, 2, 2, 3, 3, 0, 0] - assert _is_timestamp_close_to_now(waveforms[1].timing.timestamp) - assert waveforms[1].sample_count == samples_to_read - assert waveforms[1].channel_name == di_multi_chan_lines_and_port_task.di_channels[1].name - assert waveforms[1]._get_signal_names() == [ - sim_6363_device.di_lines[1].name, - sim_6363_device.di_lines[2].name, - ] - assert _get_waveform_data(waveforms[2]) == [0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - assert _is_timestamp_close_to_now(waveforms[2].timing.timestamp) - assert waveforms[2].sample_count == samples_to_read - assert waveforms[2].channel_name == di_multi_chan_lines_and_port_task.di_channels[2].name - assert waveforms[2]._get_signal_names() == [ - sim_6363_device.di_lines[3].name, - sim_6363_device.di_lines[4].name, - sim_6363_device.di_lines[5].name, - sim_6363_device.di_lines[6].name, - ] - # Note, the data on the port's waveform is MSB instead of LSB because of bug AB#3178052 - # When that bug is fixed, these asserts should be updated - assert _get_waveform_data(waveforms[3]) == [0, 128, 64, 192, 32, 160, 96, 224, 16, 144] - assert _is_timestamp_close_to_now(waveforms[3].timing.timestamp) - assert waveforms[3].sample_count == samples_to_read - assert waveforms[3].channel_name == di_multi_chan_lines_and_port_task.di_channels[3].name - assert waveforms[3]._get_signal_names() == [ - sim_6363_device.di_lines[39].name, - sim_6363_device.di_lines[38].name, - sim_6363_device.di_lines[37].name, - sim_6363_device.di_lines[36].name, - sim_6363_device.di_lines[35].name, - sim_6363_device.di_lines[34].name, - sim_6363_device.di_lines[33].name, - sim_6363_device.di_lines[32].name, - ] - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_different_lines_reader___read_mismatched_waveforms___throws_exception( - di_multi_chan_diff_lines_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_chan_diff_lines_timing_task.in_stream) - samples_to_read = 10 - waveforms = [ - DigitalWaveform(samples_to_read, 1), - DigitalWaveform( - samples_to_read, 3 - ), # mismatch - actually only two signals for this channel - DigitalWaveform(samples_to_read, 4), - ] - - with pytest.raises(ValueError) as exc_info: - reader.read_waveforms(waveforms, samples_to_read) - - error_message = str(exc_info.value) - assert "waveforms[1].data has 3 signals, but expected 2" in error_message - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader___read_waveforms_no_args___returns_valid_waveforms( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) - num_channels = di_multi_chan_multi_line_timing_task.number_of_channels - num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) - waveforms = [DigitalWaveform(50) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms) - - assert samples_read == 50 - assert num_channels == 8 - assert num_lines == 8 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan, waveform in enumerate(waveforms): - assert _get_waveform_data(waveform) == _get_expected_data_for_line(50, chan) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name - assert waveform.sample_count == 50 - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader___read_waveforms_in_place___populates_valid_waveforms( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) - num_channels = di_multi_chan_multi_line_timing_task.number_of_channels - num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) - samples_to_read = 10 - - waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 8 - assert num_lines == 8 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan, waveform in enumerate(waveforms): - assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader___reuse_waveform_in_place___overwrites_data_timing_and_attributes( - generate_task: Callable[[], nidaqmx.Task], sim_6363_device: nidaqmx.system.Device -) -> None: - def _make_multi_channel_multi_line_reader(lines_start, rate): - task = generate_task() - task.di_channels.add_di_chan( - flatten_channel_string( - sim_6363_device.di_lines.channel_names[lines_start : lines_start + 4] - ), - line_grouping=LineGrouping.CHAN_PER_LINE, - ) - task.timing.cfg_samp_clk_timing(rate, sample_mode=AcquisitionType.FINITE, samps_per_chan=10) - return DigitalMultiChannelReader(task.in_stream) - - sample_count = 10 - num_channels = 4 - reader0 = _make_multi_channel_multi_line_reader(lines_start=0, rate=1000.0) - reader1 = _make_multi_channel_multi_line_reader(lines_start=1, rate=2000.0) - waveforms = [DigitalWaveform(sample_count) for _ in range(num_channels)] - - reader0.read_waveforms(waveforms, sample_count) - timestamps0 = [wf.timing.timestamp for wf in waveforms] - for chan, waveform in enumerate(waveforms): - assert _get_waveform_data(waveform) == _get_expected_data_for_line(sample_count, chan) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == sim_6363_device.di_lines[chan].name - - reader1.read_waveforms(waveforms, sample_count) - timestamps1 = [wf.timing.timestamp for wf in waveforms] - for chan, waveform in enumerate(waveforms): - assert _get_waveform_data(waveform) == _get_expected_data_for_line(sample_count, chan + 1) - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 2000) - assert waveform.channel_name == sim_6363_device.di_lines[chan + 1].name - - for ts0, ts1 in zip(timestamps0, timestamps1): - assert ts1 > ts0 - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader___read_into_undersized_waveforms___throws_exception( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) - num_channels = di_multi_chan_multi_line_timing_task.number_of_channels - samples_to_read = 10 - - waveforms = [DigitalWaveform(samples_to_read - 1) for _ in range(num_channels)] - with pytest.raises(DaqError) as exc_info: - reader.read_waveforms(waveforms, samples_to_read) - - assert exc_info.value.error_code == DAQmxErrors.READ_BUFFER_TOO_SMALL - assert exc_info.value.args[0].startswith("The waveform at index 0 does not have enough space") - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader___read_with_wrong_number_of_waveforms___throws_exception( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - reader = DigitalMultiChannelReader(di_multi_chan_multi_line_timing_task.in_stream) - num_channels = di_multi_chan_multi_line_timing_task.number_of_channels - samples_to_read = 10 - - waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels - 1)] - with pytest.raises(DaqError) as exc_info: - reader.read_waveforms(waveforms, samples_to_read) - - assert exc_info.value.error_code == DAQmxErrors.MISMATCHED_INPUT_ARRAY_SIZES - assert "does not match the number of channels" in exc_info.value.args[0] - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader_with_timing_flag___read_waveforms___only_includes_timing_data( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - in_stream = di_multi_chan_multi_line_timing_task.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.TIMING - reader = DigitalMultiChannelReader(in_stream) - num_channels = di_multi_chan_multi_line_timing_task.number_of_channels - num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) - samples_to_read = 10 - waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 8 - assert num_lines == 8 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan, waveform in enumerate(waveforms): - assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == "" - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader_with_extended_properties_flag___read_waveforms___only_includes_extended_properties( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - in_stream = di_multi_chan_multi_line_timing_task.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.EXTENDED_PROPERTIES - reader = DigitalMultiChannelReader(in_stream) - num_channels = di_multi_chan_multi_line_timing_task.number_of_channels - num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) - samples_to_read = 10 - waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 8 - assert num_lines == 8 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan, waveform in enumerate(waveforms): - assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE - assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader_with_both_flags___read_waveforms___includes_both_timing_and_extended_properties( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - in_stream = di_multi_chan_multi_line_timing_task.in_stream - in_stream.waveform_attribute_mode = ( - WaveformAttributeMode.TIMING | WaveformAttributeMode.EXTENDED_PROPERTIES - ) - reader = DigitalMultiChannelReader(in_stream) - num_channels = di_multi_chan_multi_line_timing_task.number_of_channels - num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) - samples_to_read = 10 - waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 8 - assert num_lines == 8 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan, waveform in enumerate(waveforms): - assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) - assert _is_timestamp_close_to_now(waveform.timing.timestamp) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.REGULAR - assert waveform.timing.sample_interval == ht_timedelta(seconds=1 / 1000) - assert waveform.channel_name == di_multi_chan_multi_line_timing_task.di_channels[chan].name - assert waveform.sample_count == samples_to_read - - -@pytest.mark.grpc_skip(reason="read_digital_waveforms not implemented in GRPC") -def test___digital_multi_channel_multi_line_reader_with_none_flag___read_waveforms___minimal_waveform_data( - di_multi_chan_multi_line_timing_task: nidaqmx.Task, -) -> None: - in_stream = di_multi_chan_multi_line_timing_task.in_stream - in_stream.waveform_attribute_mode = WaveformAttributeMode.NONE - reader = DigitalMultiChannelReader(in_stream) - num_channels = di_multi_chan_multi_line_timing_task.number_of_channels - num_lines = _get_num_lines_in_task(di_multi_chan_multi_line_timing_task) - samples_to_read = 10 - waveforms = [DigitalWaveform(samples_to_read) for _ in range(num_channels)] - - samples_read = reader.read_waveforms(waveforms, samples_to_read) - - assert samples_read == samples_to_read - assert num_channels == 8 - assert num_lines == 8 - assert isinstance(waveforms, list) - assert len(waveforms) == num_channels - for chan, waveform in enumerate(waveforms): - assert _get_waveform_data(waveform) == _get_expected_data_for_line(samples_to_read, chan) - assert waveform.timing.sample_interval_mode == SampleIntervalMode.NONE - assert waveform.channel_name == "" - assert waveform.sample_count == samples_to_read