Skip to content

Commit de65044

Browse files
mikeprosserniMike Prosser
andauthored
[releases/1.0] cherry pick: Reverse Indices for Digital Waveform Signals (#222) (#233)
* Reverse Indices for Digital Waveform Signals (#222) * add _raw_index * line_index and line_names * more tests * fix new test * data_index and documentation * fix doc types * documentation improvements * documentation improvements * documentation improvements * clean up NI_LineNames in tests * remove DigitalWaveformFailure.data_index * test___signal_with_line_names___change_line_names_property___signal_returns_new_line_name and _DigitalWaveformExtendedProperties * signal_column_index * line lengths * italics for 'See "Signal index vs. signal column index"' notes * add bitorder to from_port, and default to the industry standard 'big' * rename to column_index * bitorder != sys.byteorder * add on_key_changed to ExtendedPropertyDictionary * change tests to big-endian * make on_key_changed private * cleanup * from typing_extensions import TypeAlias, to fix python 3.9 * fix pickling and copying issues with callbacks * change on_key_changed to a list of weak methods, so ExtendedPropertyDictionaries can be shared by waveforms --------- Co-authored-by: Mike Prosser <[email protected]> * Fix unpickling issues with Waveform and Signal (#237) * add versioned unpickling tests for waveform and signal, and fix issues causing the new tests to fail * skip unpickle tests in oldest_deps test run --------- Co-authored-by: Mike Prosser <[email protected]> --------- Co-authored-by: Mike Prosser <[email protected]>
1 parent e06aec0 commit de65044

File tree

7 files changed

+731
-155
lines changed

7 files changed

+731
-155
lines changed

src/nitypes/waveform/_digital/_port.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

3+
import sys
34
from collections.abc import Sequence
5+
from typing import Literal
46

57
import numpy as np
68
import numpy.typing as npt
@@ -76,15 +78,28 @@ def _get_port_dtype(mask: int) -> np.dtype[AnyDigitalPort]:
7678
)
7779

7880

79-
def port_to_line_data(port_data: npt.NDArray[AnyDigitalPort], mask: int) -> npt.NDArray[np.uint8]:
81+
def port_to_line_data(
82+
port_data: npt.NDArray[AnyDigitalPort], mask: int, bitorder: Literal["big", "little"] = "big"
83+
) -> npt.NDArray[np.uint8]:
8084
"""Convert a 1D array of port data to a 2D array of line data, using the specified mask.
8185
8286
>>> port_to_line_data(np.array([0,1,2,3], np.uint8), 0xFF)
87+
array([[0, 0, 0, 0, 0, 0, 0, 0],
88+
[0, 0, 0, 0, 0, 0, 0, 1],
89+
[0, 0, 0, 0, 0, 0, 1, 0],
90+
[0, 0, 0, 0, 0, 0, 1, 1]], dtype=uint8)
91+
92+
>>> port_to_line_data(np.array([0,1,2,3], np.uint8), 0xFF, bitorder="little")
8393
array([[0, 0, 0, 0, 0, 0, 0, 0],
8494
[1, 0, 0, 0, 0, 0, 0, 0],
8595
[0, 1, 0, 0, 0, 0, 0, 0],
8696
[1, 1, 0, 0, 0, 0, 0, 0]], dtype=uint8)
8797
>>> port_to_line_data(np.array([0,1,2,3], np.uint8), 0x3)
98+
array([[0, 0],
99+
[0, 1],
100+
[1, 0],
101+
[1, 1]], dtype=uint8)
102+
>>> port_to_line_data(np.array([0,1,2,3], np.uint8), 0x3, bitorder="little")
88103
array([[0, 0],
89104
[1, 0],
90105
[0, 1],
@@ -97,29 +112,42 @@ def port_to_line_data(port_data: npt.NDArray[AnyDigitalPort], mask: int) -> npt.
97112
>>> port_to_line_data(np.array([0,1,2,3], np.uint8), 0)
98113
array([], shape=(4, 0), dtype=uint8)
99114
>>> port_to_line_data(np.array([0x12000000,0xFE000000], np.uint32), 0xFF000000)
100-
array([[0, 1, 0, 0, 1, 0, 0, 0],
101-
[0, 1, 1, 1, 1, 1, 1, 1]], dtype=uint8)
115+
array([[0, 0, 0, 1, 0, 0, 1, 0],
116+
[1, 1, 1, 1, 1, 1, 1, 0]], dtype=uint8)
102117
"""
103118
port_size = port_data.dtype.itemsize * 8
104-
line_data = np.unpackbits(port_data.view(np.uint8), bitorder="little")
119+
# Convert to big-endian byte order to ensure MSB comes first when bitorder='big'
120+
# For multi-byte types on little-endian systems, we need to byteswap
121+
if bitorder != sys.byteorder and port_data.dtype.itemsize > 1:
122+
port_data = port_data.byteswap()
123+
124+
line_data = np.unpackbits(port_data.view(np.uint8), bitorder=bitorder)
105125
line_data = line_data.reshape(len(port_data), port_size)
106126

107127
if mask == bit_mask(port_size):
108128
return line_data
109129
else:
110-
return line_data[:, _mask_to_line_indices(mask)]
130+
return line_data[:, _mask_to_column_indices(mask, port_size, bitorder)]
111131

112132

113-
def _mask_to_line_indices(mask: int, /) -> list[int]:
114-
"""Return the line indices for the given mask.
133+
def _mask_to_column_indices(
134+
mask: int, port_size: int, bitorder: Literal["big", "little"], /
135+
) -> list[int]:
136+
"""Return the column indices for the given mask.
115137
116-
>>> _mask_to_line_indices(0xF)
138+
>>> _mask_to_column_indices(0xF, 8, "big")
139+
[4, 5, 6, 7]
140+
>>> _mask_to_column_indices(0x100, 16, "big")
141+
[7]
142+
>>> _mask_to_column_indices(0xDEADBEEF, 32, "big")
143+
[0, 1, 3, 4, 5, 6, 8, 10, 12, 13, 15, 16, 18, 19, 20, 21, 22, 24, 25, 26, 28, 29, 30, 31]
144+
>>> _mask_to_column_indices(0xF, 8, "little")
117145
[0, 1, 2, 3]
118-
>>> _mask_to_line_indices(0x100)
146+
>>> _mask_to_column_indices(0x100, 16, "little")
119147
[8]
120-
>>> _mask_to_line_indices(0xDEADBEEF)
148+
>>> _mask_to_column_indices(0xDEADBEEF, 32, "little")
121149
[0, 1, 2, 3, 5, 6, 7, 9, 10, 11, 12, 13, 15, 16, 18, 19, 21, 23, 25, 26, 27, 28, 30, 31]
122-
>>> _mask_to_line_indices(-1)
150+
>>> _mask_to_column_indices(-1, 8)
123151
Traceback (most recent call last):
124152
...
125153
ValueError: The mask must be a non-negative integer.
@@ -128,11 +156,18 @@ def _mask_to_line_indices(mask: int, /) -> list[int]:
128156
"""
129157
if mask < 0:
130158
raise ValueError("The mask must be a non-negative integer.\n\n" f"Mask: {mask}")
131-
line_indices = []
132-
line_index = 0
159+
column_indices = []
160+
bit_position = 0
133161
while mask != 0:
134162
if mask & 1:
135-
line_indices.append(line_index)
136-
line_index += 1
163+
if bitorder == "big":
164+
column_indices.append(port_size - 1 - bit_position)
165+
else: # little
166+
column_indices.append(bit_position)
167+
bit_position += 1
137168
mask >>= 1
138-
return line_indices
169+
170+
if bitorder == "big":
171+
column_indices.reverse()
172+
173+
return column_indices

src/nitypes/waveform/_digital/_signal.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,26 @@ class DigitalWaveformSignal(Generic[TDigitalState]):
2323
collection, e.g. ``waveform.signals[0]`` or ``waveform.signals["Dev1/port0/line0"]``.
2424
"""
2525

26-
__slots__ = ["_owner", "_signal_index", "__weakref__"]
26+
__slots__ = ["_owner", "_signal_index", "_column_index", "__weakref__"]
2727

2828
_owner: DigitalWaveform[TDigitalState]
29+
_column_index: int
2930
_signal_index: int
3031

31-
def __init__(self, owner: DigitalWaveform[TDigitalState], signal_index: SupportsIndex) -> None:
32+
def __init__(
33+
self,
34+
owner: DigitalWaveform[TDigitalState],
35+
signal_index: SupportsIndex,
36+
column_index: SupportsIndex | None = None,
37+
) -> None:
3238
"""Initialize a new digital waveform signal."""
39+
if column_index is None:
40+
# when unpickling an old version, column_index may not be provided
41+
column_index = signal_index
42+
3343
self._owner = owner
3444
self._signal_index = arg_to_uint("signal index", signal_index)
45+
self._column_index = arg_to_uint("column index", column_index)
3546

3647
@property
3748
def owner(self) -> DigitalWaveform[TDigitalState]:
@@ -40,22 +51,36 @@ def owner(self) -> DigitalWaveform[TDigitalState]:
4051

4152
@property
4253
def signal_index(self) -> int:
43-
"""The signal index."""
54+
"""The signal's position in the DigitalWaveform.signals collection (0-based)."""
4455
return self._signal_index
4556

57+
@property
58+
def column_index(self) -> int:
59+
"""The signal's position in the DigitalWaveform.data array's second dimension (0-based).
60+
61+
This index is used to access the signal's data within the waveform's data array:
62+
`waveform.data[:, column_index]`.
63+
64+
Note: The column_index is reversed compared to the signal_index. column_index 0 (the
65+
leftmost column) corresponds to the highest signal_index and highest line number. The
66+
highest column_index (the rightmost column) corresponds to signal_index 0 and line 0. This
67+
matches industry conventions where line 0 is the LSB and appears as the rightmost bit.
68+
"""
69+
return self._column_index
70+
4671
@property
4772
def data(self) -> npt.NDArray[TDigitalState]:
4873
"""The signal data, indexed by sample."""
49-
return self._owner.data[:, self._signal_index]
74+
return self._owner.data[:, self._column_index]
5075

5176
@property
5277
def name(self) -> str:
5378
"""The signal name."""
54-
return self._owner._get_signal_name(self._signal_index)
79+
return self._owner._get_line_name(self._column_index)
5580

5681
@name.setter
5782
def name(self, value: str) -> None:
58-
self._owner._set_signal_name(self._signal_index, value)
83+
self._owner._set_line_name(self._column_index, value)
5984

6085
def __eq__(self, value: object, /) -> bool:
6186
"""Return self==value."""
@@ -66,7 +91,7 @@ def __eq__(self, value: object, /) -> bool:
6691

6792
def __reduce__(self) -> tuple[Any, ...]:
6893
"""Return object state for pickling."""
69-
ctor_args = (self._owner, self._signal_index)
94+
ctor_args = (self._owner, self._signal_index, self._column_index)
7095
return (self.__class__, ctor_args)
7196

7297
def __repr__(self) -> str:

src/nitypes/waveform/_digital/_signal_collection.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,25 @@ def __getitem__(
4949
self, index: int | str | slice
5050
) -> DigitalWaveformSignal[TDigitalState] | Sequence[DigitalWaveformSignal[TDigitalState]]:
5151
"""Get self[index]."""
52-
if isinstance(index, int):
52+
if isinstance(index, int): # index is the signal index
5353
if index < 0:
5454
index += len(self._signals)
5555
value = self._signals[index]
5656
if value is None:
57-
value = self._signals[index] = DigitalWaveformSignal(self._owner, index)
57+
column_index = self._owner._reverse_index(index)
58+
value = self._signals[index] = DigitalWaveformSignal(
59+
self._owner, index, column_index
60+
)
5861
return value
59-
elif isinstance(index, str):
60-
signal_names = self._owner._get_signal_names()
62+
elif isinstance(index, str): # index is the line name
63+
line_names = self._owner._get_line_names()
6164
try:
62-
signal_index = signal_names.index(index)
65+
column_index = line_names.index(index)
6366
except ValueError:
6467
raise IndexError(index)
68+
signal_index = self._owner._reverse_index(column_index)
6569
return self[signal_index]
66-
elif isinstance(index, slice):
70+
elif isinstance(index, slice): # index is a slice of signal indices
6771
return [self[i] for i in range(*index.indices(len(self)))]
6872
else:
6973
raise invalid_arg_type("index", "int or str", index)

0 commit comments

Comments
 (0)