Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
autoapi_options = list(autoapi.extension._DEFAULT_OPTIONS)
autoapi_options.remove("private-members") # note: remove this to include "_" members in docs
autoapi_dirs = [root_path / "src" / "nitypes"]
autoapi_python_class_content = "both"
autoapi_type = "python"
autodoc_typehints = "description"

Expand Down
49 changes: 46 additions & 3 deletions src/nitypes/_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,39 @@ def arg_to_uint(
return value


def is_dtype(dtype: npt.DTypeLike, supported_dtypes: tuple[npt.DTypeLike, ...]) -> bool:
"""Check a dtype-like object against a tuple of supported dtype-like objects.

Unlike :any:`numpy.isdtype`, this function supports structured data types.

>>> is_dtype(np.float64, (np.float64, np.intc, np.long,))
True
>>> is_dtype("float64", (np.float64, np.intc, np.long,))
True
>>> is_dtype(np.float64, (np.byte, np.short, np.intc, np.int_, np.long, np.longlong))
False
>>> a_type = np.dtype([('a', np.int32)])
>>> b_type = np.dtype([('b', np.int32)])
>>> is_dtype(a_type, (np.float64, np.intc, a_type,))
True
>>> is_dtype(b_type, (np.float64, np.intc, a_type,))
False
>>> is_dtype("i2, i2", (np.float64, np.intc, a_type,))
False
>>> is_dtype("i4", (np.float64, np.intc, a_type,))
False
>>> is_dtype("i4", (np.float64, np.intc, a_type, np.dtype("i4"),))
True
"""
if not isinstance(dtype, (type, np.dtype)):
dtype = np.dtype(dtype)

if isinstance(dtype, np.dtype) and dtype.fields:
return dtype in supported_dtypes

return np.isdtype(dtype, supported_dtypes)


def validate_dtype(dtype: npt.DTypeLike, supported_dtypes: tuple[npt.DTypeLike, ...]) -> None:
"""Validate a dtype-like object against a tuple of supported dtype-like objects.

Expand All @@ -145,10 +178,20 @@ def validate_dtype(dtype: npt.DTypeLike, supported_dtypes: tuple[npt.DTypeLike,
<BLANKLINE>
Data type: float64
Supported data types: int8, int16, int32, int64
>>> a_type = np.dtype([('a', np.int32)])
>>> b_type = np.dtype([('b', np.int32)])
>>> validate_dtype(a_type, (np.float64, np.intc, a_type,))
>>> validate_dtype(b_type, (np.float64, np.intc, a_type,))
Traceback (most recent call last):
...
TypeError: The requested data type is not supported.
<BLANKLINE>
Data type: [('b', '<i4')]
Supported data types: float64, int32, void32
"""
if not isinstance(dtype, (type, np.dtype)):
dtype = np.dtype(dtype)
if not np.isdtype(dtype, supported_dtypes):
if not is_dtype(dtype, supported_dtypes):
if not isinstance(dtype, (type, np.dtype)):
dtype = np.dtype(dtype)
raise unsupported_dtype("requested data type", dtype, supported_dtypes)


Expand Down
96 changes: 94 additions & 2 deletions src/nitypes/waveform/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,99 @@
"""Waveform data types for NI Python APIs."""
"""Waveform data types for NI Python APIs.

from nitypes.waveform._analog_waveform import AnalogWaveform
Analog Waveforms
================

An analog waveform represents a single analog signal with timing information and extended
properties such as units.

Constructing analog waveforms
-----------------------------

To construct an analog waveform, use the :any:`AnalogWaveform` class:

>>> AnalogWaveform()
nitypes.waveform.AnalogWaveform(0)
>>> AnalogWaveform(5)
nitypes.waveform.AnalogWaveform(5, raw_data=array([0., 0., 0., 0., 0.]))

To construct an analog waveform from a NumPy array, use the :any:`AnalogWaveform.from_array_1d`
method.

>>> import numpy as np
>>> AnalogWaveform.from_array_1d(np.array([1.0, 2.0, 3.0]))
nitypes.waveform.AnalogWaveform(3, raw_data=array([1., 2., 3.]))

You can also use :any:`AnalogWaveform.from_array_1d` to construct an analog waveform from a
sequence, such as a list. In this case, you must specify the NumPy data type.

>>> AnalogWaveform.from_array_1d([1.0, 2.0, 3.0], np.float64)
nitypes.waveform.AnalogWaveform(3, raw_data=array([1., 2., 3.]))

The 2D version, :any:`AnalogWaveform.from_array_2d`, constructs a list of waveforms, one for each
row of data in the array or nested sequence.

>>> nested_list = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
>>> AnalogWaveform.from_array_2d(nested_list, np.float64) # doctest: +NORMALIZE_WHITESPACE
[nitypes.waveform.AnalogWaveform(3, raw_data=array([1., 2., 3.])),
nitypes.waveform.AnalogWaveform(3, raw_data=array([4., 5., 6.]))]

Scaling analog data
-------------------

By default, analog waveforms contain floating point data in :any:`numpy.float64` format, but they
can also be used to scale raw integer data to floating-point:

>>> scale_mode = LinearScaleMode(gain=2.0, offset=0.5)
>>> wfm = AnalogWaveform.from_array_1d([1, 2, 3], np.int32, scale_mode=scale_mode)
>>> wfm # doctest: +NORMALIZE_WHITESPACE
nitypes.waveform.AnalogWaveform(3, int32, raw_data=array([1, 2, 3], dtype=int32),
scale_mode=nitypes.waveform.LinearScaleMode(2.0, 0.5))
>>> wfm.raw_data
array([1, 2, 3], dtype=int32)
>>> wfm.scaled_data
array([2.5, 4.5, 6.5])

Complex Waveforms
=================

A complex waveform represents a single complex-number signal, such as I/Q data, with timing
information and extended properties such as units.

Constructing complex waveforms
------------------------------

To construct a complex waveform, use the :any:`ComplexWaveform` class:

>>> ComplexWaveform.from_array_1d([1 + 2j, 3 + 4j], np.complex128)
nitypes.waveform.ComplexWaveform(2, complex128, raw_data=array([1.+2.j, 3.+4.j]))

Scaling complex-number data
---------------------------

Complex waveforms support scaling raw integer data to floating-point. Python and NumPy do not
have native support for complex integers, so this uses the :any:`ComplexInt32DType` structured data
type.

>>> from nitypes.complex import ComplexInt32DType
>>> wfm = ComplexWaveform.from_array_1d([(1, 2), (3, 4)], ComplexInt32DType, scale_mode=scale_mode)
>>> wfm # doctest: +NORMALIZE_WHITESPACE
nitypes.waveform.ComplexWaveform(2, void32, raw_data=array([(1, 2), (3, 4)],
dtype=[('real', '<i2'), ('imag', '<i2')]),
scale_mode=nitypes.waveform.LinearScaleMode(2.0, 0.5))
>>> wfm.raw_data
array([(1, 2), (3, 4)], dtype=[('real', '<i2'), ('imag', '<i2')])
>>> wfm.scaled_data
array([2.5+4.j, 6.5+8.j])
"""

from nitypes.waveform._analog import AnalogWaveform
from nitypes.waveform._complex import ComplexWaveform
from nitypes.waveform._exceptions import TimingMismatchError
from nitypes.waveform._extended_properties import (
ExtendedPropertyDictionary,
ExtendedPropertyValue,
)
from nitypes.waveform._numeric import NumericWaveform
from nitypes.waveform._scaling import (
NO_SCALING,
LinearScaleMode,
Expand All @@ -23,11 +111,13 @@
__all__ = [
"AnalogWaveform",
"BaseTiming",
"ComplexWaveform",
"ExtendedPropertyDictionary",
"ExtendedPropertyValue",
"LinearScaleMode",
"NO_SCALING",
"NoneScaleMode",
"NumericWaveform",
"PrecisionTiming",
"SampleIntervalMode",
"ScaleMode",
Expand All @@ -40,11 +130,13 @@
# Hide that it was defined in a helper file
AnalogWaveform.__module__ = __name__
BaseTiming.__module__ = __name__
ComplexWaveform.__module__ = __name__
ExtendedPropertyDictionary.__module__ = __name__
# ExtendedPropertyValue is a TypeAlias
LinearScaleMode.__module__ = __name__
# NO_SCALING is a constant
NoneScaleMode.__module__ = __name__
NumericWaveform.__module__ = __name__
PrecisionTiming.__module__ = __name__
SampleIntervalMode.__module__ = __name__
ScaleMode.__module__ = __name__
Expand Down
Loading