88import numpy as np
99from copy import deepcopy
1010from warnings import warn
11+ import re
1112from . import config
1213
1314__all__ = ['issiso' , 'timebase' , 'common_timebase' , 'timebaseEqual' ,
2223 'namedio.linearized_system_name_suffix' : '$linearized' ,
2324 'namedio.sampled_system_name_prefix' : '' ,
2425 'namedio.sampled_system_name_suffix' : '$sampled' ,
26+ 'namedio.indexed_system_name_prefix' : '' ,
27+ 'namedio.indexed_system_name_suffix' : '$indexed' ,
2528 'namedio.converted_system_name_prefix' : '' ,
2629 'namedio.converted_system_name_suffix' : '$converted' ,
2730}
2831
2932
3033class NamedIOSystem (object ):
3134 def __init__ (
32- self , name = None , inputs = None , outputs = None , states = None , ** kwargs ):
35+ self , name = None , inputs = None , outputs = None , states = None ,
36+ input_prefix = 'u' , output_prefix = 'y' , state_prefix = 'x' , ** kwargs ):
3337
3438 # system name
3539 self .name = self ._name_or_default (name )
3640
3741 # Parse and store the number of inputs and outputs
38- self .set_inputs (inputs )
39- self .set_outputs (outputs )
40- self .set_states (states )
42+ self .set_inputs (inputs , prefix = input_prefix )
43+ self .set_outputs (outputs , prefix = output_prefix )
44+ self .set_states (states , prefix = state_prefix )
4145
4246 # Process timebase: if not given use default, but allow None as value
4347 self .dt = _process_dt_keyword (kwargs )
@@ -56,6 +60,9 @@ def _name_or_default(self, name=None, prefix_suffix_name=None):
5660 if name is None :
5761 name = "sys[{}]" .format (NamedIOSystem ._idCounter )
5862 NamedIOSystem ._idCounter += 1
63+ elif re .match (r".*\..*" , name ):
64+ raise ValueError (f"invalid system name '{ name } ' ('.' not allowed)" )
65+
5966 prefix = "" if prefix_suffix_name is None else config .defaults [
6067 'namedio.' + prefix_suffix_name + '_system_name_prefix' ]
6168 suffix = "" if prefix_suffix_name is None else config .defaults [
@@ -64,7 +71,6 @@ def _name_or_default(self, name=None, prefix_suffix_name=None):
6471
6572 # Check if system name is generic
6673 def _generic_name_check (self ):
67- import re
6874 return re .match (r'^sys\[\d*\]$' , self .name ) is not None
6975
7076 #
@@ -106,6 +112,39 @@ def __str__(self):
106112 def _find_signal (self , name , sigdict ):
107113 return sigdict .get (name , None )
108114
115+ # Find a list of signals by name, index, or pattern
116+ def _find_signals (self , name_list , sigdict ):
117+ if not isinstance (name_list , (list , tuple )):
118+ name_list = [name_list ]
119+
120+ index_list = []
121+ for name in name_list :
122+ # Look for signal ranges (slice-like or base name)
123+ ms = re .match (r'([\w$]+)\[([\d]*):([\d]*)\]$' , name ) # slice
124+ mb = re .match (r'([\w$]+)$' , name ) # base
125+ if ms :
126+ base = ms .group (1 )
127+ start = None if ms .group (2 ) == '' else int (ms .group (2 ))
128+ stop = None if ms .group (3 ) == '' else int (ms .group (3 ))
129+ for var in sigdict :
130+ # Find variables that match
131+ msig = re .match (r'([\w$]+)\[([\d]+)\]$' , var )
132+ if msig and msig .group (1 ) == base and \
133+ (start is None or int (msig .group (2 )) >= start ) and \
134+ (stop is None or int (msig .group (2 )) < stop ):
135+ index_list .append (sigdict .get (var ))
136+ elif mb and sigdict .get (name , None ) is None :
137+ # Try to use name as a base name
138+ for var in sigdict :
139+ msig = re .match (name + r'\[([\d]+)\]$' , var )
140+ if msig :
141+ index_list .append (sigdict .get (var ))
142+ else :
143+ index_list .append (sigdict .get (name , None ))
144+
145+ return None if len (index_list ) == 0 or \
146+ any ([idx is None for idx in index_list ]) else index_list
147+
109148 def _copy_names (self , sys , prefix = "" , suffix = "" , prefix_suffix_name = None ):
110149 """copy the signal and system name of sys. Name is given as a keyword
111150 in case a specific name (e.g. append 'linearized') is desired. """
@@ -151,7 +190,6 @@ def copy(self, name=None, use_prefix_suffix=True):
151190 return newsys
152191
153192 def set_inputs (self , inputs , prefix = 'u' ):
154-
155193 """Set the number/names of the system inputs.
156194
157195 Parameters
@@ -175,6 +213,10 @@ def find_input(self, name):
175213 """Find the index for an input given its name (`None` if not found)"""
176214 return self .input_index .get (name , None )
177215
216+ def find_inputs (self , name_list ):
217+ """Return list of indices matching input spec (`None` if not found)"""
218+ return self ._find_signals (name_list , self .input_index )
219+
178220 # Property for getting and setting list of input signals
179221 input_labels = property (
180222 lambda self : list (self .input_index .keys ()), # getter
@@ -204,6 +246,10 @@ def find_output(self, name):
204246 """Find the index for an output given its name (`None` if not found)"""
205247 return self .output_index .get (name , None )
206248
249+ def find_outputs (self , name_list ):
250+ """Return list of indices matching output spec (`None` if not found)"""
251+ return self ._find_signals (name_list , self .output_index )
252+
207253 # Property for getting and setting list of output signals
208254 output_labels = property (
209255 lambda self : list (self .output_index .keys ()), # getter
@@ -227,12 +273,16 @@ def set_states(self, states, prefix='x'):
227273
228274 """
229275 self .nstates , self .state_index = \
230- _process_signal_list (states , prefix = prefix )
276+ _process_signal_list (states , prefix = prefix , allow_dot = True )
231277
232278 def find_state (self , name ):
233279 """Find the index for a state given its name (`None` if not found)"""
234280 return self .state_index .get (name , None )
235281
282+ def find_states (self , name_list ):
283+ """Return list of indices matching state spec (`None` if not found)"""
284+ return self ._find_signals (name_list , self .state_index )
285+
236286 # Property for getting and setting list of state signals
237287 state_labels = property (
238288 lambda self : list (self .state_index .keys ()), # getter
@@ -578,7 +628,7 @@ def _process_dt_keyword(keywords, defaults={}, static=False):
578628
579629
580630# Utility function to parse a list of signals
581- def _process_signal_list (signals , prefix = 's' ):
631+ def _process_signal_list (signals , prefix = 's' , allow_dot = False ):
582632 if signals is None :
583633 # No information provided; try and make it up later
584634 return None , {}
@@ -589,10 +639,17 @@ def _process_signal_list(signals, prefix='s'):
589639
590640 elif isinstance (signals , str ):
591641 # Single string given => single signal with given name
642+ if not allow_dot and re .match (r".*\..*" , signals ):
643+ raise ValueError (
644+ f"invalid signal name '{ signals } ' ('.' not allowed)" )
592645 return 1 , {signals : 0 }
593646
594647 elif all (isinstance (s , str ) for s in signals ):
595648 # Use the list of strings as the signal names
649+ for signal in signals :
650+ if not allow_dot and re .match (r".*\..*" , signal ):
651+ raise ValueError (
652+ f"invalid signal name '{ signal } ' ('.' not allowed)" )
596653 return len (signals ), {signals [i ]: i for i in range (len (signals ))}
597654
598655 else :
0 commit comments