-
Notifications
You must be signed in to change notification settings - Fork 3
Add TimeDeltaArray to bintime #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jfriedri-ni
merged 70 commits into
main
from
users/jfriedri/ab3137071-add-bintime-array-wrappers
Aug 8, 2025
Merged
Changes from 69 commits
Commits
Show all changes
70 commits
Select commit
Hold shift + click to select a range
9487985
Add sketch of basic TimeDelta array
jfriedri-ni b263c5f
Do not incorrectly swap the struct packing order
jfriedri-ni 3772dc4
Specialize the numpy storage as much as supported
jfriedri-ni d73ae19
Stop using Union
jfriedri-ni 9a15209
Use len() for a 1D numpy array
jfriedri-ni 5dda9f0
Test with negative and fractional values as well
jfriedri-ni 25dd201
Add CVI representation helpers to TimeValueTuple
jfriedri-ni 6626785
Fix docstring typo
jfriedri-ni 3645d6a
Use the CVI helper when getting an item
jfriedri-ni d25b88f
Try using a generator when constructing the TimeDeltaArray
jfriedri-ni 0bdac3d
Address formatter to get to the type checker
jfriedri-ni 7af2a9a
Iterate over the input sequence when constructing the np array
jfriedri-ni f35b7ff
Add a test for indexing by integer
jfriedri-ni 268351f
Use np.fromiter() to initialize the backing ndarray
jfriedri-ni 6a6861e
Add test for len()
jfriedri-ni 363740a
Add TimeDeltaArray constructor benchmark tests
jfriedri-ni 56f0d7c
Promote to MutableSequence
jfriedri-ni 27a0c91
Add signatures for remaining MutableSequence methods
jfriedri-ni 37470da
Add impl and test for slicing
jfriedri-ni 30dda3a
Add test for indexing with invalid indices
jfriedri-ni 27faef7
Add test cases for negative index and out of bounds index
jfriedri-ni c1a2265
Implement and test __setitem__()
jfriedri-ni f116a78
Implement and test __delitem__()
jfriedri-ni 13fa2a1
Implement and test insert()
jfriedri-ni 1600306
Validate and test constructor argument
jfriedri-ni 4f421e6
Test constructing from another TimeDeltaArray
jfriedri-ni 67db372
Test MutableSequence mixin methods
jfriedri-ni dceb88c
Implement and test builtin functions
jfriedri-ni cc7fd8a
Address formatter
jfriedri-ni afa6e19
Optimize when slicing
jfriedri-ni 57d39af
Clarify error messages
jfriedri-ni 0228bf0
Implement and test __imul__()
jfriedri-ni f1d20e2
Remove antipattern from benchmark test
jfriedri-ni 0a86f24
Update README and .gitignore to support comparing benchmarks
jfriedri-ni 356d82e
Group the constructor benchmarks into a single test with parametrize
jfriedri-ni 69ae183
Add docstrings from Claude Sonnet 4
jfriedri-ni d613342
Remove implementation details and example usage from method docstrings
jfriedri-ni 4865b2c
Add TimeDeltaArray to module docstring
jfriedri-ni 192ea1c
Address the analyzers
jfriedri-ni 9f2aac4
Fix docstring tests
jfriedri-ni b45507b
Switch to google-style docstrings
jfriedri-ni 55c524e
Remove unnecessary detail from descriptions, rely on default docgen b…
jfriedri-ni 2d011fd
Remove the how-to-use narrative section
jfriedri-ni f280f97
Guess and check -- was it this one?
jfriedri-ni 6935936
Allow indexing with bools
jfriedri-ni 547d051
Use object for __eq__'s type hint on other
jfriedri-ni 4b6e25a
Use flattened strings for test parameter declarations
jfriedri-ni c5b6fde
Make test section comments match other files' style
jfriedri-ni b3169c2
Remove __imul__ and tests
jfriedri-ni e504a6f
Use error factories for user input validation
jfriedri-ni 21f3b09
Fix test name typo
jfriedri-ni 7a9f9a2
Validate that an unpickled TimeDeltaArray has its own backing array
jfriedri-ni 01a2aa4
Remove test for sorted
jfriedri-ni 03ffd4e
Add test for deepcopy()
jfriedri-ni 17c090f
Use pytest parameters for validating equals
jfriedri-ni 7b91f69
TDD: use full truth-table coverage for setting with a slice
jfriedri-ni 4891ab4
Make TimeDeltaArray behave like list when setting by slice
jfriedri-ni fdb40bb
Unpack the slice into named variables
jfriedri-ni 9e3c67a
Prefer builtin functions over magic methods
jfriedri-ni 8543def
Group the slice assignment tests together
jfriedri-ni 3b198e0
Add test cases for settings mismatched-length values on contiguous sl…
jfriedri-ni 89b857a
Raise when trying to shrink the array using a stride
jfriedri-ni 896abc8
Do not hokey-pokey when setting a single value with a slice
jfriedri-ni 8c87415
Group selected==incoming test cases
jfriedri-ni 83e6bcf
Remove 'expected to raise' test cases that should pass for plain lists
jfriedri-ni f9ff477
Group and add mismatched slice vs incoming test cases
jfriedri-ni c891490
Address the analyzers
jfriedri-ni 26a18ba
Contiguous slices accept any number of incoming entries
jfriedri-ni 176aa33
Implement plain old list slice assignment
jfriedri-ni 157d3e4
Use np.insert to grow the array once
jfriedri-ni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Collection, Iterable, MutableSequence | ||
| from typing import ( | ||
| TYPE_CHECKING, | ||
| Any, | ||
| final, | ||
| overload, | ||
| ) | ||
|
|
||
| import numpy as np | ||
| import numpy.typing as npt | ||
|
|
||
| from nitypes._exceptions import invalid_arg_value, invalid_arg_type | ||
|
|
||
| if TYPE_CHECKING: | ||
| # Import from the public package so the docs don't reference private submodules. | ||
| from nitypes.bintime import CVITimeIntervalDType, TimeDelta, TimeValueTuple | ||
| else: | ||
| from nitypes.bintime._dtypes import CVITimeIntervalDType | ||
| from nitypes.bintime._timedelta import TimeDelta | ||
| from nitypes.bintime._time_value_tuple import TimeValueTuple | ||
|
|
||
|
|
||
| @final | ||
| class TimeDeltaArray(MutableSequence[TimeDelta]): | ||
| """A mutable array of :class:`TimeDelta` values in NI Binary Time Format (NI-BTF). | ||
|
|
||
| Raises: | ||
| TypeError: If any item in value is not a TimeDelta instance. | ||
| """ | ||
|
|
||
| __slots__ = ["_array"] | ||
|
|
||
| _array: npt.NDArray[np.void] | ||
|
|
||
| def __init__( | ||
| self, | ||
| value: Collection[TimeDelta] | None = None, | ||
| ) -> None: | ||
| """Initialize a new TimeDeltaArray.""" | ||
| if value is None: | ||
| value = [] | ||
| if not all(isinstance(item, TimeDelta) for item in value): | ||
| raise invalid_arg_type("value", "iterable of TimeDelta", value) | ||
| self._array = np.fromiter( | ||
| (entry.to_tuple().to_cvi() for entry in value), | ||
| dtype=CVITimeIntervalDType, | ||
| count=len(value), | ||
| ) | ||
|
|
||
| @overload | ||
| def __getitem__( # noqa: D105 - missing docstring in magic method | ||
| self, index: int | ||
| ) -> TimeDelta: ... | ||
|
|
||
| @overload | ||
| def __getitem__( # noqa: D105 - missing docstring in magic method | ||
| self, index: slice | ||
| ) -> TimeDeltaArray: ... | ||
|
|
||
| def __getitem__(self, index: int | slice) -> TimeDelta | TimeDeltaArray: | ||
| """Return self[index]. | ||
|
|
||
| Raises: | ||
| TypeError: If index is an invalid type. | ||
| IndexError: If index is out of range. | ||
| """ | ||
| if isinstance(index, int): | ||
| entry = self._array[index].item() | ||
| as_tuple = TimeValueTuple.from_cvi(*entry) | ||
| return TimeDelta.from_tuple(as_tuple) | ||
| elif isinstance(index, slice): | ||
| sliced_entries = self._array[index] | ||
| new_array = TimeDeltaArray() | ||
| new_array._array = sliced_entries | ||
| return new_array | ||
| else: | ||
| raise invalid_arg_type("index", "int or slice", index) | ||
|
|
||
| def __len__(self) -> int: | ||
| """Return len(self).""" | ||
| return len(self._array) | ||
|
|
||
| @overload | ||
| def __setitem__( # noqa: D105 - missing docstring in magic method | ||
| self, index: int, value: TimeDelta | ||
| ) -> None: ... | ||
|
|
||
| @overload | ||
| def __setitem__( # noqa: D105 - missing docstring in magic method | ||
| self, index: slice, value: Iterable[TimeDelta] | ||
| ) -> None: ... | ||
|
|
||
| def __setitem__(self, index: int | slice, value: TimeDelta | Iterable[TimeDelta]) -> None: | ||
| """Set a new value for TimeDelta at the specified location or slice. | ||
|
|
||
| Raises: | ||
| TypeError: If index is an invalid type, or slice value is not iterable. | ||
| ValueError: If slice assignment length doesn't match the selected range. | ||
| IndexError: If index is out of range. | ||
| """ | ||
| if isinstance(index, int): | ||
| if not isinstance(value, TimeDelta): | ||
| raise invalid_arg_type("value", "TimeDelta", value) | ||
| self._array[index] = value.to_tuple().to_cvi() | ||
| elif isinstance(index, slice): | ||
| if not isinstance(value, Iterable): | ||
| raise invalid_arg_type("value", "iterable of TimeDelta", value) | ||
| if not all(isinstance(item, TimeDelta) for item in value): | ||
| raise invalid_arg_type("value", "iterable of TimeDelta", value) | ||
|
|
||
| start, stop, step = index.indices(len(self)) | ||
| selected_count = len(range(start, stop, step)) | ||
| values = list(value) | ||
| new_entry_count = len(values) | ||
| if step > 1 and new_entry_count != selected_count: | ||
| raise invalid_arg_value( | ||
| "value", "iterable with the same length as the slice", value | ||
| ) | ||
|
|
||
| if new_entry_count < selected_count: | ||
| # Shrink | ||
| replaced = slice(start, start + new_entry_count) | ||
| removed = slice(start + new_entry_count, stop) | ||
| self._array[replaced] = [item.to_tuple().to_cvi() for item in values] | ||
| del self[removed] | ||
| elif new_entry_count > selected_count: | ||
| # Grow | ||
| replaced = slice(start, stop) | ||
| self._array[replaced] = [ | ||
| item.to_tuple().to_cvi() for item in values[:selected_count] | ||
| ] | ||
| for offset, entry in enumerate(values[selected_count:]): | ||
| self.insert(stop + offset, entry) | ||
jfriedri-ni marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| else: | ||
| # Replace, accounting for strides | ||
| self._array[index] = [item.to_tuple().to_cvi() for item in values] | ||
| else: | ||
| raise invalid_arg_type("index", "int or slice", index) | ||
|
|
||
| @overload | ||
| def __delitem__(self, index: int) -> None: ... # noqa: D105 - missing docstring in magic method | ||
|
|
||
| @overload | ||
| def __delitem__( # noqa: D105 - missing docstring in magic method | ||
| self, index: slice | ||
| ) -> None: ... | ||
|
|
||
| def __delitem__(self, index: int | slice) -> None: | ||
| """Delete the value at the specified location or slice. | ||
|
|
||
| Raises: | ||
| TypeError: If index is an invalid type. | ||
| IndexError: If index is out of range. | ||
| """ | ||
| if isinstance(index, (int, slice)): | ||
| self._array = np.delete(self._array, index) | ||
| else: | ||
| raise invalid_arg_type("index", "int or slice", index) | ||
|
|
||
| def insert(self, index: int, value: TimeDelta) -> None: | ||
| """Insert the TimeDelta value before the specified index. | ||
|
|
||
| Raises: | ||
| TypeError: If index is not int or value is not TimeDelta. | ||
| """ | ||
| if not isinstance(index, int): | ||
| raise invalid_arg_type("index", "int", index) | ||
| if not isinstance(value, TimeDelta): | ||
| raise invalid_arg_type("value", "TimeDelta", value) | ||
| lower = -len(self._array) | ||
| upper = len(self._array) | ||
| index = min(max(index, lower), upper) | ||
| as_cvi = value.to_tuple().to_cvi() | ||
| self._array = np.insert(self._array, index, as_cvi) | ||
|
|
||
| def __eq__(self, other: object) -> bool: | ||
| """Return self == other.""" | ||
| if not isinstance(other, TimeDeltaArray): | ||
| return NotImplemented | ||
| return np.array_equal(self._array, other._array) | ||
|
|
||
| def __reduce__(self) -> tuple[Any, ...]: | ||
| """Return object state for pickling.""" | ||
| return (self.__class__, (list(iter(self)),)) | ||
|
|
||
| def __repr__(self) -> str: | ||
| """Return repr(self).""" | ||
| ctor_args = list(iter(self)) | ||
| return f"{self.__class__.__module__}.{self.__class__.__name__}({ctor_args})" | ||
|
|
||
| def __str__(self) -> str: | ||
| """Return str(self).""" | ||
| values = list(iter(self)) | ||
| return f"[{'; '.join(str(v) for v in values)}]" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
| from pytest_benchmark.fixture import BenchmarkFixture | ||
|
|
||
| import nitypes.bintime as bt | ||
|
|
||
|
|
||
| LIST_10: list[bt.TimeDelta] = [bt.TimeDelta(float(value)) for value in np.arange(-10, 10, 0.3)] | ||
| LIST_100: list[bt.TimeDelta] = [bt.TimeDelta(float(value)) for value in np.arange(-100, 100, 0.3)] | ||
| LIST_1000: list[bt.TimeDelta] = [ | ||
| bt.TimeDelta(float(value)) for value in np.arange(-1000, 1000, 0.3) | ||
| ] | ||
| LIST_10000: list[bt.TimeDelta] = [ | ||
| bt.TimeDelta(float(value)) for value in np.arange(-10000, 10000, 0.3) | ||
| ] | ||
|
|
||
|
|
||
| @pytest.mark.benchmark(group="timedelta_array_construct", min_rounds=100) | ||
| @pytest.mark.parametrize("constructor_list", (LIST_10, LIST_100, LIST_1000, LIST_10000)) | ||
| def test___bt_timedelta_array___construct( | ||
| benchmark: BenchmarkFixture, | ||
| constructor_list: list[bt.TimeDelta], | ||
| ) -> None: | ||
| benchmark(bt.TimeDeltaArray, constructor_list) | ||
bkeryan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.