diff --git a/control/iosys.py b/control/iosys.py index dca00d3e5..22159fc49 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -591,13 +591,8 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6, DeprecationWarning) if copy_names: - linsys._copy_names(self) - if name is None: - linsys.name = \ - config.defaults['namedio.linearized_system_name_prefix']+\ - linsys.name+\ - config.defaults['namedio.linearized_system_name_suffix'] - else: + linsys._copy_names(self, prefix_suffix_name='linearized') + if name is not None: linsys.name = name # re-init to include desired signal names if names were provided @@ -2400,7 +2395,9 @@ def ss(*args, **kwargs): "non-unique state space realization") # Create a state space system from an LTI system - sys = LinearIOSystem(_convert_to_statespace(sys), **kwargs) + sys = LinearIOSystem( + _convert_to_statespace(sys, use_prefix_suffix=True), **kwargs) + else: raise TypeError("ss(sys): sys must be a StateSpace or " "TransferFunction object. It is %s." % type(sys)) diff --git a/control/namedio.py b/control/namedio.py index 8f61514fe..c0d5f11d5 100644 --- a/control/namedio.py +++ b/control/namedio.py @@ -12,6 +12,7 @@ __all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', 'isdtime', 'isctime'] + # Define module default parameter values _namedio_defaults = { 'namedio.state_name_delim': '_', @@ -20,7 +21,9 @@ 'namedio.linearized_system_name_prefix': '', 'namedio.linearized_system_name_suffix': '$linearized', 'namedio.sampled_system_name_prefix': '', - 'namedio.sampled_system_name_suffix': '$sampled' + 'namedio.sampled_system_name_suffix': '$sampled', + 'namedio.converted_system_name_prefix': '', + 'namedio.converted_system_name_suffix': '$converted', } @@ -49,11 +52,15 @@ def __init__( _idCounter = 0 # Counter for creating generic system name # Return system name - def _name_or_default(self, name=None): + def _name_or_default(self, name=None, prefix_suffix_name=None): if name is None: name = "sys[{}]".format(NamedIOSystem._idCounter) NamedIOSystem._idCounter += 1 - return name + prefix = "" if prefix_suffix_name is None else config.defaults[ + 'namedio.' + prefix_suffix_name + '_system_name_prefix'] + suffix = "" if prefix_suffix_name is None else config.defaults[ + 'namedio.' + prefix_suffix_name + '_system_name_suffix'] + return prefix + name + suffix # Check if system name is generic def _generic_name_check(self): @@ -99,16 +106,24 @@ def __str__(self): def _find_signal(self, name, sigdict): return sigdict.get(name, None) - def _copy_names(self, sys): + def _copy_names(self, sys, prefix="", suffix="", prefix_suffix_name=None): """copy the signal and system name of sys. Name is given as a keyword in case a specific name (e.g. append 'linearized') is desired. """ - self.name = sys.name - self.ninputs, self.input_index = \ - sys.ninputs, sys.input_index.copy() - self.noutputs, self.output_index = \ - sys.noutputs, sys.output_index.copy() - self.nstates, self.state_index = \ - sys.nstates, sys.state_index.copy() + # Figure out the system name and assign it + if prefix == "" and prefix_suffix_name is not None: + prefix = config.defaults[ + 'namedio.' + prefix_suffix_name + '_system_name_prefix'] + if suffix == "" and prefix_suffix_name is not None: + suffix = config.defaults[ + 'namedio.' + prefix_suffix_name + '_system_name_suffix'] + self.name = prefix + sys.name + suffix + + # Name the inputs, outputs, and states + self.input_index = sys.input_index.copy() + self.output_index = sys.output_index.copy() + if self.nstates and sys.nstates: + # only copy state names for state space systems + self.state_index = sys.state_index.copy() def copy(self, name=None, use_prefix_suffix=True): """Make a copy of an input/output system @@ -128,10 +143,8 @@ def copy(self, name=None, use_prefix_suffix=True): # Update the system name if name is None and use_prefix_suffix: # Get the default prefix and suffix to use - dup_prefix = config.defaults['namedio.duplicate_system_name_prefix'] - dup_suffix = config.defaults['namedio.duplicate_system_name_suffix'] newsys.name = self._name_or_default( - dup_prefix + self.name + dup_suffix) + self.name, prefix_suffix_name='duplicate') else: newsys.name = self._name_or_default(name) diff --git a/control/statesp.py b/control/statesp.py index f74f39e26..d3d1ab1d0 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1382,13 +1382,8 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, sysd = StateSpace(Ad, Bd, C, D, Ts) # copy over the system name, inputs, outputs, and states if copy_names: - sysd._copy_names(self) - if name is None: - sysd.name = \ - config.defaults['namedio.sampled_system_name_prefix'] +\ - sysd.name + \ - config.defaults['namedio.sampled_system_name_suffix'] - else: + sysd._copy_names(self, prefix_suffix_name='sampled') + if name is not None: sysd.name = name # pass desired signal names if names were provided return StateSpace(sysd, **kwargs) @@ -1524,7 +1519,7 @@ def output(self, t, x, u=None, params=None): # TODO: add discrete time check -def _convert_to_statespace(sys): +def _convert_to_statespace(sys, use_prefix_suffix=False): """Convert a system to state space form (if needed). If sys is already a state space, then it is returned. If sys is a @@ -1561,11 +1556,10 @@ def _convert_to_statespace(sys): denorder, den, num, tol=0) states = ssout[0] - return StateSpace( + newsys = StateSpace( ssout[1][:states, :states], ssout[2][:states, :sys.ninputs], - ssout[3][:sys.noutputs, :states], ssout[4], sys.dt, - inputs=sys.input_labels, outputs=sys.output_labels, - name=sys.name) + ssout[3][:sys.noutputs, :states], ssout[4], sys.dt) + except ImportError: # No Slycot. Scipy tf->ss can't handle MIMO, but static # MIMO is an easy special case we can check for here @@ -1578,9 +1572,7 @@ def _convert_to_statespace(sys): for i, j in itertools.product(range(sys.noutputs), range(sys.ninputs)): D[i, j] = sys.num[i][j][0] / sys.den[i][j][0] - return StateSpace([], [], [], D, sys.dt, - inputs=sys.input_labels, outputs=sys.output_labels, - name=sys.name) + newsys = StateSpace([], [], [], D, sys.dt) else: if sys.ninputs != 1 or sys.noutputs != 1: raise TypeError("No support for MIMO without slycot") @@ -1590,9 +1582,13 @@ def _convert_to_statespace(sys): # the squeeze A, B, C, D = \ sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den)) - return StateSpace( - A, B, C, D, sys.dt, inputs=sys.input_labels, - outputs=sys.output_labels, name=sys.name) + newsys = StateSpace(A, B, C, D, sys.dt) + + # Copy over the signal (and system) names + newsys._copy_names( + sys, + prefix_suffix_name='converted' if use_prefix_suffix else None) + return newsys elif isinstance(sys, FrequencyResponseData): raise TypeError("Can't convert FRD to StateSpace system.") @@ -1605,7 +1601,6 @@ def _convert_to_statespace(sys): except Exception: raise TypeError("Can't convert given type to StateSpace system.") - # TODO: add discrete time option def _rss_generate( states, inputs, outputs, cdtype, strictly_proper=False, name=None): @@ -1919,7 +1914,8 @@ def tf2ss(*args, **kwargs): if not isinstance(sys, TransferFunction): raise TypeError("tf2ss(sys): sys must be a TransferFunction " "object.") - return StateSpace(_convert_to_statespace(sys), **kwargs) + return StateSpace(_convert_to_statespace(sys, use_prefix_suffix=True), + **kwargs) else: raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index 3d2f0c7d7..cf59c8c13 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -246,23 +246,54 @@ def test_string_inputoutput(): assert P_s2.output_index == {'y2' : 0} def test_linear_interconnect(): - tf_ctrl = ct.tf(1, (10.1, 1), inputs='e', outputs='u') - tf_plant = ct.tf(1, (10.1, 1), inputs='u', outputs='y') - ss_ctrl = ct.ss(1, 2, 1, 2, inputs='e', outputs='u') - ss_plant = ct.ss(1, 2, 1, 2, inputs='u', outputs='y') + tf_ctrl = ct.tf(1, (10.1, 1), inputs='e', outputs='u', name='ctrl') + tf_plant = ct.tf(1, (10.1, 1), inputs='u', outputs='y', name='plant') + ss_ctrl = ct.ss(1, 2, 1, 0, inputs='e', outputs='u', name='ctrl') + ss_plant = ct.ss(1, 2, 1, 0, inputs='u', outputs='y', name='plant') nl_ctrl = ct.NonlinearIOSystem( - lambda t, x, u, params: x*x, - lambda t, x, u, params: u*x, states=1, inputs='e', outputs='u') + lambda t, x, u, params: x*x, lambda t, x, u, params: u*x, + states=1, inputs='e', outputs='u', name='ctrl') nl_plant = ct.NonlinearIOSystem( - lambda t, x, u, params: x*x, - lambda t, x, u, params: u*x, states=1, inputs='u', outputs='y') - - assert isinstance(ct.interconnect((tf_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem) - assert isinstance(ct.interconnect((ss_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem) - assert isinstance(ct.interconnect((tf_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem) - assert isinstance(ct.interconnect((ss_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem) - - assert ~isinstance(ct.interconnect((nl_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem) - assert ~isinstance(ct.interconnect((nl_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem) - assert ~isinstance(ct.interconnect((ss_ctrl, nl_plant), inputs='e', outputs='y'), ct.LinearIOSystem) - assert ~isinstance(ct.interconnect((tf_ctrl, nl_plant), inputs='e', outputs='y'), ct.LinearIOSystem) \ No newline at end of file + lambda t, x, u, params: x*x, lambda t, x, u, params: u*x, + states=1, inputs='u', outputs='y', name='plant') + sumblk = ct.summing_junction(inputs=['r', '-y'], outputs=['e'], name='sum') + + # Interconnections of linear I/O systems should be linear I/O system + assert isinstance( + ct.interconnect([tf_ctrl, tf_plant, sumblk], inputs='r', outputs='y'), + ct.LinearIOSystem) + assert isinstance( + ct.interconnect([ss_ctrl, ss_plant, sumblk], inputs='r', outputs='y'), + ct.LinearIOSystem) + assert isinstance( + ct.interconnect([tf_ctrl, ss_plant, sumblk], inputs='r', outputs='y'), + ct.LinearIOSystem) + assert isinstance( + ct.interconnect([ss_ctrl, tf_plant, sumblk], inputs='r', outputs='y'), + ct.LinearIOSystem) + + # Interconnections with nonliner I/O systems should not be linear + assert ~isinstance( + ct.interconnect([nl_ctrl, ss_plant, sumblk], inputs='r', outputs='y'), + ct.LinearIOSystem) + assert ~isinstance( + ct.interconnect([nl_ctrl, tf_plant, sumblk], inputs='r', outputs='y'), + ct.LinearIOSystem) + assert ~isinstance( + ct.interconnect([ss_ctrl, nl_plant, sumblk], inputs='r', outputs='y'), + ct.LinearIOSystem) + assert ~isinstance( + ct.interconnect([tf_ctrl, nl_plant, sumblk], inputs='r', outputs='y'), + ct.LinearIOSystem) + + # Implicit converstion of transfer function should retain name + clsys = ct.interconnect( + [tf_ctrl, ss_plant, sumblk], + connections=[ + ['plant.u', 'ctrl.u'], + ['ctrl.e', 'sum.e'], + ['sum.y', 'plant.y'] + ], + inplist=['sum.r'], inputs='r', + outlist=['plant.y'], outputs='y') + assert clsys.syslist[0].name == 'ctrl' diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index 3203214d6..cf30b94aa 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -169,6 +169,9 @@ def test_io_naming(fun, args, kwargs): assert sys_ss != sys_r assert sys_ss.input_labels == input_labels assert sys_ss.output_labels == output_labels + if not isinstance(sys_r, ct.StateSpace): + # System should get unique name + assert sys_ss.name != sys_r.name # Reassign system and signal names sys_ss = ct.ss( @@ -247,11 +250,11 @@ def test_convert_to_statespace(): # check that name, inputs, and outputs passed through sys_new = ct.ss(sys) - assert sys_new.name == 'sys' + assert sys_new.name == 'sys$converted' assert sys_new.input_labels == ['u'] assert sys_new.output_labels == ['y'] sys_new = ct.ss(sys_static) - assert sys_new.name == 'sys_static' + assert sys_new.name == 'sys_static$converted' assert sys_new.input_labels == ['u'] assert sys_new.output_labels == ['y'] diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index ebb781b89..7d561e770 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -1262,6 +1262,12 @@ def test_copy_names(create, args, kwargs, convert): if cpy.nstates is not None and sys.nstates is not None: assert cpy.state_labels == sys.state_labels + # Make sure that names aren't the same if system changed type + if not isinstance(cpy, create): + assert cpy.name == sys.name + '$converted' + else: + assert cpy.name == sys.name + # Relabel inputs and outputs cpy = convert(sys, inputs='myin', outputs='myout') assert cpy.input_labels == ['myin'] diff --git a/control/xferfcn.py b/control/xferfcn.py index 8ef7e1084..60a40eca0 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1206,13 +1206,8 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, sysd = TransferFunction(numd[0, :], dend, Ts) # copy over the system name, inputs, outputs, and states if copy_names: - sysd._copy_names(self) - if name is None: - sysd.name = \ - config.defaults['namedio.sampled_system_name_prefix'] +\ - sysd.name + \ - config.defaults['namedio.sampled_system_name_suffix'] - else: + sysd._copy_names(self, prefix_suffix_name='sampled') + if name is not None: sysd.name = name # pass desired signal names if names were provided return TransferFunction(sysd, name=name, **kwargs) @@ -1438,7 +1433,8 @@ def _add_siso(num1, den1, num2, den2): return num, den -def _convert_to_transfer_function(sys, inputs=1, outputs=1): +def _convert_to_transfer_function( + sys, inputs=1, outputs=1, use_prefix_suffix=False): """Convert a system to transfer function form (if needed). If sys is already a transfer function, then it is returned. If sys is a @@ -1514,7 +1510,10 @@ def _convert_to_transfer_function(sys, inputs=1, outputs=1): num = squeeze(num) # Convert to 1D array den = squeeze(den) # Probably not needed - return TransferFunction(num, den, sys.dt) + newsys = TransferFunction(num, den, sys.dt) + if use_prefix_suffix: + newsys._copy_names(sys, prefix_suffix_name='converted') + return newsys elif isinstance(sys, (int, float, complex, np.number)): num = [[[sys] for j in range(inputs)] for i in range(outputs)] @@ -1814,7 +1813,8 @@ def ss2tf(*args, **kwargs): if not kwargs.get('outputs'): kwargs['outputs'] = sys.output_labels return TransferFunction( - _convert_to_transfer_function(sys), **kwargs) + _convert_to_transfer_function(sys, use_prefix_suffix=True), + **kwargs) else: raise TypeError( "ss2tf(sys): sys must be a StateSpace object. It is %s."