Repository navigation
Allow signal names to be used for time/freq responses and subsystem indexing #1069
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
Changes from 11 commits
5d7fb42
5df6fb7
b382abf
58922ce
e52fca6
cca4d13
944b6a2
cd3d315
df33002
1903055
fba414d
7ef09a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,9 +13,10 @@ | |||||||
| import numpy as np | ||||||||
|
|
||||||||
| from . import config | ||||||||
| from .exception import ControlIndexError | ||||||||
|
|
||||||||
| __all__ = ['InputOutputSystem', 'issiso', 'timebase', 'common_timebase', | ||||||||
| 'isdtime', 'isctime'] | ||||||||
| __all__ = ['InputOutputSystem', 'NamedSignal', 'issiso', 'timebase', | ||||||||
| 'common_timebase', 'isdtime', 'isctime'] | ||||||||
|
|
||||||||
| # Define module default parameter values | ||||||||
| _iosys_defaults = { | ||||||||
|
|
@@ -33,6 +34,68 @@ | |||||||
| } | ||||||||
|
|
||||||||
|
|
||||||||
| # Named signal class | ||||||||
| class NamedSignal(np.ndarray): | ||||||||
| def __new__(cls, input_array, signal_labels=None, trace_labels=None): | ||||||||
| # See https://numpy.org/doc/stable/user/basics.subclassing.html | ||||||||
| obj = np.asarray(input_array).view(cls) # Cast to our class type | ||||||||
| obj.signal_labels = signal_labels # Save signal labels | ||||||||
| obj.trace_labels = trace_labels # Save trace labels | ||||||||
| obj.data_shape = input_array.shape # Save data shape | ||||||||
| return obj # Return new object | ||||||||
|
|
||||||||
| def __array_finalize__(self, obj): | ||||||||
| # See https://numpy.org/doc/stable/user/basics.subclassing.html | ||||||||
| if obj is None: return | ||||||||
| self.signal_labels = getattr(obj, 'signal_labels', None) | ||||||||
| self.trace_labels = getattr(obj, 'trace_labels', None) | ||||||||
| self.data_shape = getattr(obj, 'data_shape', None) | ||||||||
|
|
||||||||
| def _parse_key(self, key, labels=None, level=0): | ||||||||
| if labels is None: | ||||||||
| labels = self.signal_labels | ||||||||
| try: | ||||||||
| if isinstance(key, str): | ||||||||
| key = labels.index(item := key) | ||||||||
| if level == 0 and len(self.data_shape) < 2: | ||||||||
| raise ControlIndexError | ||||||||
| elif isinstance(key, list): | ||||||||
| keylist = [] | ||||||||
| for item in key: # use for loop to save item for error | ||||||||
| keylist.append( | ||||||||
| self._parse_key(item, labels=labels, level=level+1)) | ||||||||
| if level == 0 and key != keylist and len(self.data_shape) < 2: | ||||||||
| raise ControlIndexError | ||||||||
| key = keylist | ||||||||
| elif isinstance(key, tuple) and len(key) > 0: | ||||||||
| keylist = [] | ||||||||
| keylist.append( | ||||||||
| self._parse_key( | ||||||||
| item := key[0], labels=self.signal_labels, | ||||||||
| level=level+1)) | ||||||||
| if len(key) > 1: | ||||||||
| keylist.append( | ||||||||
| self._parse_key( | ||||||||
| item := key[1], labels=self.trace_labels, | ||||||||
| level=level+1)) | ||||||||
| if level == 0 and key[:len(keylist)] != tuple(keylist) \ | ||||||||
| and len(keylist) > len(self.data_shape) - 1: | ||||||||
| raise ControlIndexError | ||||||||
| for i in range(2, len(key)): | ||||||||
| keylist.append(key[i]) # pass on remaining elements | ||||||||
| key = tuple(keylist) | ||||||||
| except ValueError: | ||||||||
| raise ValueError(f"unknown signal name '{item}'") | ||||||||
| except ControlIndexError: | ||||||||
| raise ControlIndexError( | ||||||||
| "signal name(s) not valid for squeezed data") | ||||||||
|
|
||||||||
| return key | ||||||||
|
|
||||||||
| def __getitem__(self, key): | ||||||||
| return super().__getitem__(self._parse_key(key)) | ||||||||
|
|
||||||||
|
|
||||||||
| class InputOutputSystem(object): | ||||||||
| """A class for representing input/output systems. | ||||||||
|
|
||||||||
|
|
@@ -965,3 +1028,30 @@ def _parse_spec(syslist, spec, signame, dictname=None): | |||||||
| ValueError(f"signal index '{index}' is out of range") | ||||||||
|
|
||||||||
| return system_index, signal_indices, gain | ||||||||
|
|
||||||||
|
|
||||||||
| # | ||||||||
| # Utility function for processing subsystem indices | ||||||||
| # | ||||||||
| # This function processes an index specification (int, list, or slice) and | ||||||||
| # returns a index specification that can be used to create a subsystem | ||||||||
| # | ||||||||
| def _process_subsys_index(idx, sys_labels, slice_to_list=False): | ||||||||
| if not isinstance(idx, (slice, list, int)): | ||||||||
| raise TypeError(f"system indices must be integers, slices, or lists") | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This string does not need the |
||||||||
|
|
||||||||
| # Convert singleton lists to integers for proper slicing (below) | ||||||||
| if isinstance(idx, (list, tuple)) and len(idx) == 1: | ||||||||
| idx = idx[0] | ||||||||
|
|
||||||||
| # Convert int to slice so that numpy doesn't drop dimension | ||||||||
| if isinstance(idx, int): idx = slice(idx, idx+1, 1) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Similar to my other comment, statements should be on separate lines for clarity of style (as per PEP 8). |
||||||||
|
|
||||||||
| # Get label names (taking care of possibility that we were passed a list) | ||||||||
| labels = [sys_labels[i] for i in idx] if isinstance(idx, list) \ | ||||||||
| else sys_labels[idx] | ||||||||
|
|
||||||||
| if slice_to_list and isinstance(idx, slice): | ||||||||
| idx = range(len(sys_labels))[idx] | ||||||||
|
|
||||||||
| return idx, labels | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returnshould go on the next line to make it more obvious what is in the if-block. Also, this is consistent with PEP 8:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example in the NumPy docs has
obj is None: return(on 1 line), but my style comment remains. I do not know why the example there has this 1-line style, but I suspect it is to keep examples short, which would also justify there being only 1 blank line afterimportin those examples.