Skip to content

Commit 55a6723

Browse files
authored
waveform: Add timing (#6)
* pyproject.toml: Use hightime dev branch for now * Update poetry.lock * waveform: Add waveform timing classes * waveform: Add WaveformTiming.get_timestamps * waveform: Add waveform timing __repr__ and rework constructors so it makes sense * waveform: Update __repr__ format and add tests * waveform: Preallocate default timedeltas * waveform: Change default sample interval to None * time: Add time conversion functions * waveform: Move timing classes to a subpackage and desmurfify the class names * waveform: Add timing conversion * waveform: Make BaseTiming weakref-able * waveform: Add AnalogWaveform timing properties * pyproject.toml: Use main branch of hightime * Update poetry.lock * conversion: Use object for singledispatch base case * conversion: Use a dict to dispatch based on requested_type * waveform: Refactor timing init args validation * waveform: Store None for default time offset * waveform: Remove unnecessary type hints * waveform: More refactoring and error cleanup * conversion: Add one more newline to errors
1 parent ae18be0 commit 55a6723

File tree

19 files changed

+1969
-16
lines changed

19 files changed

+1969
-16
lines changed

poetry.lock

Lines changed: 9 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ numpy = [
1414
{version = ">=1.26", python = ">=3.12,<3.13"},
1515
{version = ">=2.1", python = "^3.13"},
1616
]
17-
hightime = "^0.2.2"
17+
# hightime = "^0.2.2"
18+
hightime = { git = "https://github.com/ni/hightime.git" }
1819

1920
[tool.poetry.group.lint.dependencies]
2021
bandit = { version = ">=1.7", extras = ["toml"] }
@@ -43,13 +44,6 @@ plugins = "numpy.typing.mypy_plugin"
4344
namespace_packages = true
4445
strict = true
4546

46-
[[tool.mypy.overrides]]
47-
module = [
48-
# https://github.com/ni/hightime/issues/4 - Add type annotations
49-
"hightime.*",
50-
]
51-
ignore_missing_imports = true
52-
5347
[tool.bandit]
5448
skips = [
5549
"B101", # assert_used

src/nitypes/time/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Time data types for NI Python APIs."""
2+
3+
from nitypes.time._conversion import convert_datetime, convert_timedelta
4+
5+
__all__ = ["convert_datetime", "convert_timedelta"]

src/nitypes/time/_conversion.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from __future__ import annotations
2+
3+
import datetime as dt
4+
import sys
5+
from collections.abc import Callable
6+
from functools import singledispatch
7+
from typing import Any, TypeVar, Union, cast
8+
9+
import hightime as ht
10+
11+
if sys.version_info >= (3, 10):
12+
from typing import TypeAlias
13+
else:
14+
from typing_extensions import TypeAlias
15+
16+
_AnyDateTime: TypeAlias = Union[dt.datetime, ht.datetime]
17+
_TDateTime = TypeVar("_TDateTime", dt.datetime, ht.datetime)
18+
19+
_AnyTimeDelta: TypeAlias = Union[dt.timedelta, ht.timedelta]
20+
_TTimeDelta = TypeVar("_TTimeDelta", dt.timedelta, ht.timedelta)
21+
22+
23+
def convert_datetime(requested_type: type[_TDateTime], value: _AnyDateTime, /) -> _TDateTime:
24+
"""Convert a datetime object to the specified type."""
25+
convert_func = _CONVERT_DATETIME_FOR_TYPE.get(requested_type)
26+
if convert_func is None:
27+
raise TypeError(
28+
"The requested type must be a datetime type.\n\n" f"Requested type: {requested_type}"
29+
)
30+
return cast(_TDateTime, convert_func(value))
31+
32+
33+
@singledispatch
34+
def _convert_to_dt_datetime(value: object, /) -> dt.datetime:
35+
raise TypeError("The value must be a datetime.\n\n" f"Provided value: {value}")
36+
37+
38+
@_convert_to_dt_datetime.register
39+
def _(value: dt.datetime, /) -> dt.datetime:
40+
return value
41+
42+
43+
@_convert_to_dt_datetime.register
44+
def _(value: ht.datetime, /) -> dt.datetime:
45+
return dt.datetime(
46+
value.year,
47+
value.month,
48+
value.day,
49+
value.hour,
50+
value.minute,
51+
value.second,
52+
value.microsecond,
53+
value.tzinfo,
54+
fold=value.fold,
55+
)
56+
57+
58+
@singledispatch
59+
def _convert_to_ht_datetime(value: object, /) -> ht.datetime:
60+
raise TypeError("The value must be a datetime.\n\n" f"Provided value: {value}")
61+
62+
63+
@_convert_to_ht_datetime.register
64+
def _(value: dt.datetime, /) -> ht.datetime:
65+
return ht.datetime(
66+
value.year,
67+
value.month,
68+
value.day,
69+
value.hour,
70+
value.minute,
71+
value.second,
72+
value.microsecond,
73+
value.tzinfo,
74+
fold=value.fold,
75+
)
76+
77+
78+
@_convert_to_ht_datetime.register
79+
def _(value: ht.datetime, /) -> ht.datetime:
80+
return value
81+
82+
83+
_CONVERT_DATETIME_FOR_TYPE: dict[type[Any], Callable[[object], object]] = {
84+
dt.datetime: _convert_to_dt_datetime,
85+
ht.datetime: _convert_to_ht_datetime,
86+
}
87+
88+
89+
def convert_timedelta(requested_type: type[_TTimeDelta], value: _AnyTimeDelta, /) -> _TTimeDelta:
90+
"""Convert a timedelta object to the specified type."""
91+
convert_func = _CONVERT_TIMEDELTA_FOR_TYPE.get(requested_type)
92+
if convert_func is None:
93+
raise TypeError(
94+
"The requested type must be a timedelta type.\n\n" f"Requested type: {requested_type}"
95+
)
96+
return cast(_TTimeDelta, convert_func(value))
97+
98+
99+
@singledispatch
100+
def _convert_to_dt_timedelta(value: object, /) -> dt.timedelta:
101+
raise TypeError("The value must be a timedelta.\n\n" f"Provided value: {value}")
102+
103+
104+
@_convert_to_dt_timedelta.register
105+
def _(value: dt.timedelta, /) -> dt.timedelta:
106+
return value
107+
108+
109+
@_convert_to_dt_timedelta.register
110+
def _(value: ht.timedelta, /) -> dt.timedelta:
111+
return dt.timedelta(value.days, value.seconds, value.microseconds)
112+
113+
114+
@singledispatch
115+
def _convert_to_ht_timedelta(value: object, /) -> ht.timedelta:
116+
raise TypeError("The value must be a timedelta.\n\n" f"Provided value: {value}")
117+
118+
119+
@_convert_to_ht_timedelta.register
120+
def _(value: dt.timedelta, /) -> ht.timedelta:
121+
return ht.timedelta(
122+
value.days,
123+
value.seconds,
124+
value.microseconds,
125+
)
126+
127+
128+
@_convert_to_ht_timedelta.register
129+
def _(value: ht.timedelta, /) -> ht.timedelta:
130+
return value
131+
132+
133+
_CONVERT_TIMEDELTA_FOR_TYPE: dict[type[Any], Callable[[object], object]] = {
134+
dt.timedelta: _convert_to_dt_timedelta,
135+
ht.timedelta: _convert_to_ht_timedelta,
136+
}

src/nitypes/waveform/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,25 @@
55
ExtendedPropertyDictionary,
66
ExtendedPropertyValue,
77
)
8+
from nitypes.waveform._timing._base import BaseTiming, SampleIntervalMode
9+
from nitypes.waveform._timing._precision import PrecisionTiming
10+
from nitypes.waveform._timing._standard import Timing
811

912
__all__ = [
1013
"AnalogWaveform",
14+
"BaseTiming",
1115
"ExtendedPropertyDictionary",
1216
"ExtendedPropertyValue",
17+
"PrecisionTiming",
18+
"SampleIntervalMode",
19+
"Timing",
1320
]
21+
22+
# Hide that it was defined in a helper file
23+
AnalogWaveform.__module__ = __name__
24+
BaseTiming.__module__ = __name__
25+
ExtendedPropertyDictionary.__module__ = __name__
26+
# ExtendedPropertyValue is a TypeAlias
27+
PrecisionTiming.__module__ = __name__
28+
SampleIntervalMode.__module__ = __name__
29+
Timing.__module__ = __name__

src/nitypes/waveform/_analog_waveform.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
UNIT_DESCRIPTION,
1313
ExtendedPropertyDictionary,
1414
)
15+
from nitypes.waveform._timing._conversion import convert_timing
16+
from nitypes.waveform._timing._precision import PrecisionTiming
17+
from nitypes.waveform._timing._standard import Timing
1518
from nitypes.waveform._utils import arg_to_uint, validate_dtype
1619

1720
if sys.version_info < (3, 10):
@@ -211,12 +214,22 @@ def from_array_2d(
211214
for i in range(len(array))
212215
]
213216

214-
__slots__ = ["_data", "_start_index", "_sample_count", "_extended_properties", "__weakref__"]
217+
__slots__ = [
218+
"_data",
219+
"_start_index",
220+
"_sample_count",
221+
"_extended_properties",
222+
"_timing",
223+
"_precision_timing",
224+
"__weakref__",
225+
]
215226

216227
_data: npt.NDArray[_ScalarType_co]
217228
_start_index: int
218229
_sample_count: int
219230
_extended_properties: ExtendedPropertyDictionary
231+
_timing: Timing | None
232+
_precision_timing: PrecisionTiming | None
220233

221234
# If neither dtype nor _data is specified, the type parameter defaults to np.float64.
222235
@overload
@@ -332,6 +345,8 @@ def _init_with_new_array(
332345
self._start_index = start_index
333346
self._sample_count = sample_count
334347
self._extended_properties = ExtendedPropertyDictionary()
348+
self._timing = Timing.empty
349+
self._precision_timing = None
335350

336351
def _init_with_provided_array(
337352
self,
@@ -384,6 +399,8 @@ def _init_with_provided_array(
384399
self._start_index = start_index
385400
self._sample_count = sample_count
386401
self._extended_properties = ExtendedPropertyDictionary()
402+
self._timing = Timing.empty
403+
self._precision_timing = None
387404

388405
@property
389406
def raw_data(self) -> npt.NDArray[_ScalarType_co]:
@@ -464,3 +481,62 @@ def unit_description(self, value: str) -> None:
464481
"The unit description must be a str.\n\n" f"Unit description: {value!r}"
465482
)
466483
self._extended_properties[UNIT_DESCRIPTION] = value
484+
485+
@property
486+
def timing(self) -> Timing:
487+
"""The timing information of the analog waveform.
488+
489+
The default value is Timing.empty.
490+
"""
491+
if self._timing is None:
492+
if self._precision_timing is PrecisionTiming.empty:
493+
self._timing = Timing.empty
494+
elif self._precision_timing is not None:
495+
self._timing = convert_timing(Timing, self._precision_timing)
496+
else:
497+
raise RuntimeError("The waveform has no timing information.")
498+
return self._timing
499+
500+
@timing.setter
501+
def timing(self, value: Timing) -> None:
502+
if not isinstance(value, Timing):
503+
raise TypeError("The timing information must be a Timing object.")
504+
self._timing = value
505+
self._precision_timing = None
506+
507+
@property
508+
def is_precision_timing_initialized(self) -> bool:
509+
"""Indicates whether the waveform's precision timing information is initialized."""
510+
return self._precision_timing is not None
511+
512+
@property
513+
def precision_timing(self) -> PrecisionTiming:
514+
"""The precision timing information of the analog waveform.
515+
516+
The default value is PrecisionTiming.empty.
517+
518+
Use AnalogWaveform.precision_timing instead of AnalogWaveform.timing to obtain timing
519+
information with higher precision than AnalogWaveform.timing. If the timing information is
520+
set using AnalogWaveform.precision_timing, then this property returns timing information
521+
with up to yoctosecond precision. If the timing information is set using
522+
AnalogWaveform.timing, then the timing information returned has up to microsecond precision.
523+
524+
Accessing this property can potentially decrease performance if the timing information is
525+
set using AnalogWaveform.timing. Use AnalogWaveform.is_precision_timing_initialized to
526+
determine if AnalogWaveform.precision_timing has been initialized.
527+
"""
528+
if self._precision_timing is None:
529+
if self._timing is Timing.empty:
530+
self._precision_timing = PrecisionTiming.empty
531+
elif self._timing is not None:
532+
self._precision_timing = convert_timing(PrecisionTiming, self._timing)
533+
else:
534+
raise RuntimeError("The waveform has no timing information.")
535+
return self._precision_timing
536+
537+
@precision_timing.setter
538+
def precision_timing(self, value: PrecisionTiming) -> None:
539+
if not isinstance(value, PrecisionTiming):
540+
raise TypeError("The precision timing information must be a PrecisionTiming object.")
541+
self._precision_timing = value
542+
self._timing = None
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Waveform timing data types for NI Python APIs."""

0 commit comments

Comments
 (0)