From 055ed394ef76b198d21d3839c04019c4305822ee Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 22 Nov 2022 21:30:12 -0800 Subject: [PATCH 1/7] initial commit that preserves signal names during cont-to-discrete transformation --- control/dtime.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/control/dtime.py b/control/dtime.py index b05d22b96..a09f5f501 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -47,7 +47,8 @@ """ -from .namedio import isctime +from .namedio import isctime, _process_namedio_keywords +from .iosys import ss from .statesp import StateSpace __all__ = ['sample_system', 'c2d'] @@ -92,9 +93,10 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None): # Make sure we have a continuous time system if not isctime(sysc): raise ValueError("First argument must be continuous time system") - - return sysc.sample(Ts, - method=method, alpha=alpha, prewarp_frequency=prewarp_frequency) + name, inputs, outputs, states, _ = _process_namedio_keywords(defaults=sysc) + return ss(sysc.sample(Ts, + method=method, alpha=alpha, prewarp_frequency=prewarp_frequency), + name=name, inputs=inputs, outputs=outputs, states=states) def c2d(sysc, Ts, method='zoh', prewarp_frequency=None): From 1fd68c7f7d1d28b800ec29926b6ff620e701dff0 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 22 Nov 2022 21:59:12 -0800 Subject: [PATCH 2/7] changed named signal handling to occur in sys.sample methods. added unit tests. --- control/dtime.py | 10 ++++------ control/statesp.py | 7 +++++-- control/tests/discrete_test.py | 13 +++++++++++++ control/xferfcn.py | 5 ++++- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/control/dtime.py b/control/dtime.py index a09f5f501..b05d22b96 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -47,8 +47,7 @@ """ -from .namedio import isctime, _process_namedio_keywords -from .iosys import ss +from .namedio import isctime from .statesp import StateSpace __all__ = ['sample_system', 'c2d'] @@ -93,10 +92,9 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None): # Make sure we have a continuous time system if not isctime(sysc): raise ValueError("First argument must be continuous time system") - name, inputs, outputs, states, _ = _process_namedio_keywords(defaults=sysc) - return ss(sysc.sample(Ts, - method=method, alpha=alpha, prewarp_frequency=prewarp_frequency), - name=name, inputs=inputs, outputs=outputs, states=states) + + return sysc.sample(Ts, + method=method, alpha=alpha, prewarp_frequency=prewarp_frequency) def c2d(sysc, Ts, method='zoh', prewarp_frequency=None): diff --git a/control/statesp.py b/control/statesp.py index af549cff6..b47a2894f 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1347,14 +1347,17 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): if not self.isctime(): raise ValueError("System must be continuous time system") - sys = (self.A, self.B, self.C, self.D) if (method == 'bilinear' or (method == 'gbt' and alpha == 0.5)) and \ prewarp_frequency is not None: Twarp = 2 * np.tan(prewarp_frequency * Ts/2)/prewarp_frequency else: Twarp = Ts + sys = (self.A, self.B, self.C, self.D) Ad, Bd, C, D, _ = cont2discrete(sys, Twarp, method, alpha) - return StateSpace(Ad, Bd, C, D, Ts) + # get and pass along same signal names + _, inputs, outputs, states, _ = _process_namedio_keywords(defaults=self) + return StateSpace(Ad, Bd, C, D, Ts, + inputs=inputs, outputs=outputs, states=states) def dcgain(self, warn_infinite=False): """Return the zero-frequency gain diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index cb0ce3c76..0842cbf59 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -446,3 +446,16 @@ def test_discrete_bode(self, tsys): np.testing.assert_array_almost_equal(omega, omega_out) np.testing.assert_array_almost_equal(mag_out, np.absolute(H_z)) np.testing.assert_array_almost_equal(phase_out, np.angle(H_z)) + + def test_signal_names(self, tsys): + "test that signal names are preserved in conversion to discrete-time" + ssc = StateSpace(tsys.siso_ss1c, + inputs='u', outputs='y', states=['a', 'b', 'c']) + ssd = ssc.sample(0.1) + tfc = TransferFunction(tsys.siso_tf1c, inputs='u', outputs='y') + tfd = tfc.sample(0.1) + assert ssd.input_labels == ['u'] + assert ssd.state_labels == ['a', 'b', 'c'] + assert ssd.output_labels == ['y'] + assert tfd.input_labels == ['u'] + assert tfd.output_labels == ['y'] \ No newline at end of file diff --git a/control/xferfcn.py b/control/xferfcn.py index d3671c533..bcb1130e2 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1149,7 +1149,10 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): else: Twarp = Ts numd, dend, _ = cont2discrete(sys, Twarp, method, alpha) - return TransferFunction(numd[0, :], dend, Ts) + # get and pass along same signal names + _, inputs, outputs, _, _ = _process_namedio_keywords(defaults=self) + return TransferFunction(numd[0, :], dend, Ts, + inputs=inputs, outputs=outputs) def dcgain(self, warn_infinite=False): """Return the zero-frequency (or DC) gain From 65e051fdedd1239141d24b955d34a214db24d10a Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Wed, 23 Nov 2022 10:51:10 -0800 Subject: [PATCH 3/7] create function to copy system names, move default name parameters to namedio, update sys.sample to enable signal names to be passed. --- control/config.py | 3 +++ control/iosys.py | 48 +++++++++++++++------------------------- control/namedio.py | 33 +++++++++++++++++++++++----- control/statesp.py | 55 ++++++++++++++++++++++++++++++++++++---------- control/xferfcn.py | 46 +++++++++++++++++++++++++++++++++----- 5 files changed, 132 insertions(+), 53 deletions(-) diff --git a/control/config.py b/control/config.py index 32f5f2eef..3f2814d42 100644 --- a/control/config.py +++ b/control/config.py @@ -97,6 +97,9 @@ def reset_defaults(): from .rlocus import _rlocus_defaults defaults.update(_rlocus_defaults) + from .namedio import _namedio_defaults + defaults.update(_namedio_defaults) + from .xferfcn import _xferfcn_defaults defaults.update(_xferfcn_defaults) diff --git a/control/iosys.py b/control/iosys.py index 9b33d2161..fd3dcd749 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -47,13 +47,7 @@ 'interconnect', 'summing_junction'] # Define module default parameter values -_iosys_defaults = { - 'iosys.state_name_delim': '_', - 'iosys.duplicate_system_name_prefix': '', - 'iosys.duplicate_system_name_suffix': '$copy', - 'iosys.linearized_system_name_prefix': '', - 'iosys.linearized_system_name_suffix': '$linearized' -} +_iosys_defaults = {} class InputOutputSystem(NamedIOSystem): @@ -515,7 +509,7 @@ def feedback(self, other=1, sign=-1, params=None): return newsys def linearize(self, x0, u0, t=0, params=None, eps=1e-6, - name=None, copy=False, **kwargs): + name=None, copy_names=False, **kwargs): """Linearize an input/output system at a given state and input. Return the linearization of an input/output system at a given state @@ -574,20 +568,14 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6, StateSpace(A, B, C, D, self.dt, remove_useless_states=False), name=name, **kwargs) - # Set the names the system, inputs, outputs, and states - if copy: + # Set the system name, inputs, outputs, and states + if copy_names: if name is None: - linsys.name = \ - config.defaults['iosys.linearized_system_name_prefix'] + \ + name = \ + config.defaults['namedio.linearized_system_name_prefix'] +\ self.name + \ - config.defaults['iosys.linearized_system_name_suffix'] - linsys.ninputs, linsys.input_index = self.ninputs, \ - self.input_index.copy() - linsys.noutputs, linsys.output_index = \ - self.noutputs, self.output_index.copy() - linsys.nstates, linsys.state_index = \ - self.nstates, self.state_index.copy() - + config.defaults['namedio.linearized_system_name_suffix'] + linsys._copy_names(self, name=name) return linsys @@ -966,7 +954,7 @@ def __init__(self, syslist, connections=None, inplist=None, outlist=None, if states is None: states = [] - state_name_delim = config.defaults['iosys.state_name_delim'] + state_name_delim = config.defaults['namedio.state_name_delim'] for sys, sysname in sysobj_name_dct.items(): states += [sysname + state_name_delim + statename for statename in sys.state_index.keys()] @@ -2192,18 +2180,18 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw): params : dict, optional Parameter values for the systems. Passed to the evaluation functions for the system as default values, overriding internal defaults. - copy : bool, Optional - If `copy` is True, copy the names of the input signals, output signals, - and states to the linearized system. If `name` is not specified, - the system name is set to the input system name with the string - '_linearized' appended. + copy_names : bool, Optional + If `copy_names` is True, copy the names of the input signals, output + signals, and states to the linearized system. If `name` is not + specified, the system name is set to the input system name with the + string '_linearized' appended. name : string, optional Set the name of the linearized system. If not specified and if `copy` is `False`, a generic name is generated with a unique integer id. If `copy` is `True`, the new system name is determined by adding the prefix and suffix strings in - config.defaults['iosys.linearized_system_name_prefix'] and - config.defaults['iosys.linearized_system_name_suffix'], with the + config.defaults['namedio.linearized_system_name_prefix'] and + config.defaults['namedio.linearized_system_name_suffix'], with the default being to add the suffix '$linearized'. Returns @@ -2728,8 +2716,8 @@ def interconnect(syslist, connections=None, inplist=None, outlist=None, If a system is duplicated in the list of systems to be connected, a warning is generated and a copy of the system is created with the name of the new system determined by adding the prefix and suffix - strings in config.defaults['iosys.linearized_system_name_prefix'] - and config.defaults['iosys.linearized_system_name_suffix'], with the + strings in config.defaults['namedio.linearized_system_name_prefix'] + and config.defaults['namedio.linearized_system_name_suffix'], with the default being to add the suffix '$copy'$ to the system name. It is possible to replace lists in most of arguments with tuples instead, diff --git a/control/namedio.py b/control/namedio.py index 254f310ff..52e68671b 100644 --- a/control/namedio.py +++ b/control/namedio.py @@ -12,7 +12,18 @@ __all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual', 'isdtime', 'isctime'] - +# Define module default parameter values +_namedio_defaults = { + 'namedio.state_name_delim': '_', + 'namedio.duplicate_system_name_prefix': '', + 'namedio.duplicate_system_name_suffix': '$copy', + 'namedio.linearized_system_name_prefix': '', + 'namedio.linearized_system_name_suffix': '$linearized', + 'namedio.sampled_system_name_prefix': '', + 'namedio.sampled_system_name_suffix': '$sampled' +} + + class NamedIOSystem(object): def __init__( self, name=None, inputs=None, outputs=None, states=None, **kwargs): @@ -88,14 +99,26 @@ def __str__(self): def _find_signal(self, name, sigdict): return sigdict.get(name, None) + def _copy_names(self, sys, 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. """ + if name is None: + 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() + def copy(self, name=None, use_prefix_suffix=True): """Make a copy of an input/output system A copy of the system is made, with a new name. The `name` keyword can be used to specify a specific name for the system. If no name is given and `use_prefix_suffix` is True, the name is constructed - by prepending config.defaults['iosys.duplicate_system_name_prefix'] - and appending config.defaults['iosys.duplicate_system_name_suffix']. + by prepending config.defaults['namedio.duplicate_system_name_prefix'] + and appending config.defaults['namedio.duplicate_system_name_suffix']. Otherwise, a generic system name of the form `sys[]` is used, where `` is based on an internal counter. @@ -106,8 +129,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['iosys.duplicate_system_name_prefix'] - dup_suffix = config.defaults['iosys.duplicate_system_name_suffix'] + 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) else: diff --git a/control/statesp.py b/control/statesp.py index b47a2894f..b5a5fe26f 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -63,8 +63,8 @@ from .exception import ControlSlycot from .frdata import FrequencyResponseData from .lti import LTI, _process_frequency_response -from .namedio import common_timebase, isdtime -from .namedio import _process_namedio_keywords +from .namedio import common_timebase, isdtime, _process_namedio_keywords, \ + _process_dt_keyword from . import config from copy import deepcopy @@ -357,9 +357,9 @@ def __init__(self, *args, init_namedio=True, **kwargs): states=states, dt=dt) elif kwargs: raise TypeError("unrecognized keyword(s): ", str(kwargs)) - + # Reset shapes (may not be needed once np.matrix support is removed) - if 0 == self.nstates: + if self._isstatic(): # static gain # matrix's default "empty" shape is 1x0 A.shape = (0, 0) @@ -1298,7 +1298,8 @@ def __getitem__(self, indices): return StateSpace(self.A, self.B[:, j], self.C[i, :], self.D[i, j], self.dt) - def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): + def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, + name=None, copy_names=True, **kwargs): """Convert a continuous time system to discrete time Creates a discrete-time system from a continuous-time system by @@ -1317,22 +1318,44 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): alpha=0) * backward_diff: Backwards differencing ("gbt" with alpha=1.0) * zoh: zero-order hold (default) - alpha : float within [0, 1] The generalized bilinear transformation weighting parameter, which should only be specified with method="gbt", and is ignored otherwise - prewarp_frequency : float within [0, infinity) The frequency [rad/s] at which to match with the input continuous- time system's magnitude and phase (the gain=1 crossover frequency, for example). Should only be specified with method='bilinear' or 'gbt' with alpha=0.5 and ignored otherwise. + copy_names : bool, Optional + If `copy_names` is True, copy the names of the input signals, output + signals, and states to the sampled system. If `name` is not + specified, the system name is set to the input system name with the + string '_sampled' appended. + name : string, optional + Set the name of the sampled system. If not specified and + if `copy` is `False`, a generic name is generated + with a unique integer id. If `copy` is `True`, the new system + name is determined by adding the prefix and suffix strings in + config.defaults['namedio.sampled_system_name_prefix'] and + config.defaults['namedio.sampled_system_name_suffix'], with the + default being to add the suffix '$sampled'. Returns ------- sysd : StateSpace - Discrete time system, with sampling rate Ts + Discrete-time system, with sampling rate Ts + + Additional Parameters + --------------------- + inputs : int, list of str or None, optional + Description of the system inputs. If not specified, the origional + system inputs are used. See :class:`InputOutputSystem` for more + information. + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. Notes ----- @@ -1354,10 +1377,18 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): Twarp = Ts sys = (self.A, self.B, self.C, self.D) Ad, Bd, C, D, _ = cont2discrete(sys, Twarp, method, alpha) - # get and pass along same signal names - _, inputs, outputs, states, _ = _process_namedio_keywords(defaults=self) - return StateSpace(Ad, Bd, C, D, Ts, - inputs=inputs, outputs=outputs, states=states) + sysd = StateSpace(Ad, Bd, C, D, Ts) + # copy over the system name, inputs, outputs, and states + if copy_names: + if name is None: + name = \ + config.defaults['namedio.sampled_system_name_prefix'] +\ + self.name + \ + config.defaults['namedio.sampled_system_name_suffix'] + sysd._copy_names(self, name=name) + # pass desired signal names if names were provided + sysd = StateSpace(sysd, **kwargs) + return sysd def dcgain(self, warn_infinite=False): """Return the zero-frequency gain diff --git a/control/xferfcn.py b/control/xferfcn.py index bcb1130e2..a5d52967a 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1090,7 +1090,8 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): return num, den, denorder - def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): + def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, + name=None, copy_names=True, **kwargs): """Convert a continuous-time system to discrete time Creates a discrete-time system from a continuous-time system by @@ -1118,11 +1119,35 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): time system's magnitude and phase (the gain=1 crossover frequency, for example). Should only be specified with method='bilinear' or 'gbt' with alpha=0.5 and ignored otherwise. + copy_names : bool, Optional + If `copy_names` is True, copy the names of the input signals, output + signals, and states to the sampled system. If `name` is not + specified, the system name is set to the input system name with the + string '_sampled' appended. + name : string, optional + Set the name of the sampled system. If not specified and + if `copy` is `False`, a generic name is generated + with a unique integer id. If `copy` is `True`, the new system + name is determined by adding the prefix and suffix strings in + config.defaults['namedio.sampled_system_name_prefix'] and + config.defaults['namedio.sampled_system_name_suffix'], with the + default being to add the suffix '$sampled'. Returns ------- sysd : TransferFunction system - Discrete time system, with sample period Ts + Discrete-time system, with sample period Ts + + Additional Parameters + --------------------- + inputs : int, list of str or None, optional + Description of the system inputs. If not specified, the origional + system inputs are used. See :class:`NamedIOSystem` for more + information. + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. Notes ----- @@ -1149,11 +1174,20 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): else: Twarp = Ts numd, dend, _ = cont2discrete(sys, Twarp, method, alpha) - # get and pass along same signal names - _, inputs, outputs, _, _ = _process_namedio_keywords(defaults=self) - return TransferFunction(numd[0, :], dend, Ts, - inputs=inputs, outputs=outputs) + sysd = TransferFunction(numd[0, :], dend, Ts) + # copy over the system name, inputs, outputs, and states + if copy_names: + if name is None: + name = \ + config.defaults['namedio.sampled_system_name_prefix'] +\ + self.name + \ + config.defaults['namedio.sampled_system_name_suffix'] + sysd._copy_names(self, name=name) + # pass desired signal names if names were provided + sysd = TransferFunction(sysd, **kwargs) + return sysd + def dcgain(self, warn_infinite=False): """Return the zero-frequency (or DC) gain From 28277f3e77b7b2871bec906af101e40f6735bce0 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Wed, 23 Nov 2022 12:09:41 -0800 Subject: [PATCH 4/7] add unit tests and fixes to pass unit tests --- control/config.py | 2 +- control/iosys.py | 2 +- control/namedio.py | 2 ++ control/tests/iosys_test.py | 8 +++---- control/tests/kwargs_test.py | 2 ++ control/tests/statesp_test.py | 40 +++++++++++++++++++++++++++++++++-- control/tests/xferfcn_test.py | 31 +++++++++++++++++++++++++++ 7 files changed, 79 insertions(+), 8 deletions(-) diff --git a/control/config.py b/control/config.py index 3f2814d42..ccee252fc 100644 --- a/control/config.py +++ b/control/config.py @@ -288,7 +288,7 @@ def use_legacy_defaults(version): set_defaults('control', default_dt=None) # changed iosys naming conventions - set_defaults('iosys', state_name_delim='.', + set_defaults('namedio', state_name_delim='.', duplicate_system_name_prefix='copy of ', duplicate_system_name_suffix='', linearized_system_name_prefix='', diff --git a/control/iosys.py b/control/iosys.py index fd3dcd749..40eaba9f5 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -2204,7 +2204,7 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw): --------------------- inputs : int, list of str or None, optional Description of the system inputs. If not specified, the origional - system inputs are used. See :class:`InputOutputSystem` for more + system inputs are used. See :class:`NamedIOSystem` for more information. outputs : int, list of str or None, optional Description of the system outputs. Same format as `inputs`. diff --git a/control/namedio.py b/control/namedio.py index 52e68671b..9f82e5929 100644 --- a/control/namedio.py +++ b/control/namedio.py @@ -104,6 +104,8 @@ def _copy_names(self, sys, name=None): in case a specific name (e.g. append 'linearized') is desired. """ if name is None: self.name = sys.name + else: + self.name = name self.ninputs, self.input_index = \ sys.ninputs, sys.input_index.copy() self.noutputs, self.output_index = \ diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 7d3b9fee9..09542bcaa 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -216,7 +216,7 @@ def test_linearize(self, tsys, kincar): @pytest.mark.usefixtures("editsdefaults") def test_linearize_named_signals(self, kincar): # Full form of the call - linearized = kincar.linearize([0, 0, 0], [0, 0], copy=True, + linearized = kincar.linearize([0, 0, 0], [0, 0], copy_names=True, name='linearized') assert linearized.name == 'linearized' assert linearized.find_input('v') == 0 @@ -228,17 +228,17 @@ def test_linearize_named_signals(self, kincar): assert linearized.find_state('theta') == 2 # If we copy signal names w/out a system name, append '$linearized' - linearized = kincar.linearize([0, 0, 0], [0, 0], copy=True) + linearized = kincar.linearize([0, 0, 0], [0, 0], copy_names=True) assert linearized.name == kincar.name + '$linearized' # Test legacy version as well ct.use_legacy_defaults('0.8.4') ct.config.use_numpy_matrix(False) # np.matrix deprecated - linearized = kincar.linearize([0, 0, 0], [0, 0], copy=True) + linearized = kincar.linearize([0, 0, 0], [0, 0], copy_names=True) assert linearized.name == kincar.name + '_linearized' # If copy is False, signal names should not be copied - lin_nocopy = kincar.linearize(0, 0, copy=False) + lin_nocopy = kincar.linearize(0, 0, copy_names=False) assert lin_nocopy.find_input('v') is None assert lin_nocopy.find_output('x') is None assert lin_nocopy.find_state('x') is None diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 2dc7f0563..065c4673f 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -198,8 +198,10 @@ def test_matplotlib_kwargs(function, nsysargs, moreargs, kwargs, mplcleanup): 'NonlinearIOSystem.__init__': interconnect_test.test_interconnect_exceptions, 'StateSpace.__init__': test_unrecognized_kwargs, + 'StateSpace.sample': test_unrecognized_kwargs, 'TimeResponseData.__call__': trdata_test.test_response_copy, 'TransferFunction.__init__': test_unrecognized_kwargs, + 'TransferFunction.sample': test_unrecognized_kwargs, } # diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 6fcdade22..41f0c893a 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -820,8 +820,42 @@ def test_error_u_dynamics_mimo(self, u, sys222): sys222.dynamics(0, (1, 1), u) with pytest.raises(ValueError): sys222.output(0, (1, 1), u) - - + + def test_sample_named_signals(self): + sysc = ct.StateSpace(1.1, 1, 1, 1, inputs='u', outputs='y', states='a') + + # Full form of the call + sysd = sysc.sample(0.1, name='sampled') + assert sysd.name == 'sampled' + assert sysd.find_input('u') == 0 + assert sysd.find_output('y') == 0 + assert sysd.find_state('a') == 0 + + # If we copy signal names w/out a system name, append '$sampled' + sysd = sysc.sample(0.1) + assert sysd.name == sysc.name + '$sampled' + + # If copy is False, signal names should not be copied + sysd_nocopy = sysc.sample(0.1, copy_names=False) + assert sysd_nocopy.find_input('u') is None + assert sysd_nocopy.find_output('y') is None + assert sysd_nocopy.find_state('a') is None + + # if signal names are provided, they should override those of sysc + sysd_newnames = sysc.sample(0.1, inputs='v', outputs='x', states='b') + assert sysd_newnames.find_input('v') == 0 + assert sysd_newnames.find_input('u') is None + assert sysd_newnames.find_output('x') == 0 + assert sysd_newnames.find_output('y') is None + assert sysd_newnames.find_state('b') == 0 + assert sysd_newnames.find_state('a') is None + # test just one name + sysd_newnames = sysc.sample(0.1, inputs='v') + assert sysd_newnames.find_input('v') == 0 + assert sysd_newnames.find_input('u') is None + assert sysd_newnames.find_output('y') == 0 + assert sysd_newnames.find_output('x') is None + class TestRss: """These are tests for the proper functionality of statesp.rss.""" @@ -1164,3 +1198,5 @@ def test_params_warning(): with pytest.warns(UserWarning, match="params keyword ignored"): sys.output(0, [0], [0], {'k': 5}) + + diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 894da5594..e4a2b3ec0 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -986,6 +986,37 @@ def test_repr(self, Hargs, ref): np.testing.assert_array_almost_equal(H.num[p][m], H2.num[p][m]) np.testing.assert_array_almost_equal(H.den[p][m], H2.den[p][m]) assert H.dt == H2.dt + + def test_sample_named_signals(self): + sysc = ct.TransferFunction(1.1, (1, 2), inputs='u', outputs='y') + + # Full form of the call + sysd = sysc.sample(0.1, name='sampled') + assert sysd.name == 'sampled' + assert sysd.find_input('u') == 0 + assert sysd.find_output('y') == 0 + + # If we copy signal names w/out a system name, append '$sampled' + sysd = sysc.sample(0.1) + assert sysd.name == sysc.name + '$sampled' + + # If copy is False, signal names should not be copied + sysd_nocopy = sysc.sample(0.1, copy_names=False) + assert sysd_nocopy.find_input('u') is None + assert sysd_nocopy.find_output('y') is None + + # if signal names are provided, they should override those of sysc + sysd_newnames = sysc.sample(0.1, inputs='v', outputs='x') + assert sysd_newnames.find_input('v') == 0 + assert sysd_newnames.find_input('u') is None + assert sysd_newnames.find_output('x') == 0 + assert sysd_newnames.find_output('y') is None + # test just one name + sysd_newnames = sysc.sample(0.1, inputs='v') + assert sysd_newnames.find_input('v') == 0 + assert sysd_newnames.find_input('u') is None + assert sysd_newnames.find_output('y') == 0 + assert sysd_newnames.find_output('x') is None class TestLTIConverter: From c34526108c58ba681a0da3f1d8fcfa6a09e448c7 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Wed, 23 Nov 2022 12:35:53 -0800 Subject: [PATCH 5/7] a few more unit tests and improvements to system name handling: --- control/iosys.py | 4 ++-- control/statesp.py | 2 +- control/tests/iosys_test.py | 14 ++++++++++++++ control/xferfcn.py | 4 ++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 40eaba9f5..5b941e964 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -565,8 +565,7 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6, # Create the state space system linsys = LinearIOSystem( - StateSpace(A, B, C, D, self.dt, remove_useless_states=False), - name=name, **kwargs) + StateSpace(A, B, C, D, self.dt, remove_useless_states=False)) # Set the system name, inputs, outputs, and states if copy_names: @@ -576,6 +575,7 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6, self.name + \ config.defaults['namedio.linearized_system_name_suffix'] linsys._copy_names(self, name=name) + linsys = LinearIOSystem(linsys, name=name, **kwargs) return linsys diff --git a/control/statesp.py b/control/statesp.py index b5a5fe26f..561b2d343 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1387,7 +1387,7 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, config.defaults['namedio.sampled_system_name_suffix'] sysd._copy_names(self, name=name) # pass desired signal names if names were provided - sysd = StateSpace(sysd, **kwargs) + sysd = StateSpace(sysd, name=name, **kwargs) return sysd def dcgain(self, warn_infinite=False): diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 09542bcaa..6bed7cd16 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -243,6 +243,20 @@ def test_linearize_named_signals(self, kincar): assert lin_nocopy.find_output('x') is None assert lin_nocopy.find_state('x') is None + # if signal names are provided, they should override those of kincar + linearized_newnames = kincar.linearize([0, 0, 0], [0, 0], + name='linearized', + copy_names=True, inputs=['v2', 'phi2'], outputs=['x2','y2']) + assert linearized_newnames.name == 'linearized' + assert linearized_newnames.find_input('v2') == 0 + assert linearized_newnames.find_input('phi2') == 1 + assert linearized_newnames.find_input('v') is None + assert linearized_newnames.find_input('phi') is None + assert linearized_newnames.find_output('x2') == 0 + assert linearized_newnames.find_output('y2') == 1 + assert linearized_newnames.find_output('x') is None + assert linearized_newnames.find_output('y') is None + def test_connect(self, tsys): # Define a couple of (linear) systems to interconnection linsys1 = tsys.siso_linsys diff --git a/control/xferfcn.py b/control/xferfcn.py index a5d52967a..79e46de7e 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1185,9 +1185,9 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, config.defaults['namedio.sampled_system_name_suffix'] sysd._copy_names(self, name=name) # pass desired signal names if names were provided - sysd = TransferFunction(sysd, **kwargs) + sysd = TransferFunction(sysd, name=name, **kwargs) return sysd - + def dcgain(self, warn_infinite=False): """Return the zero-frequency (or DC) gain From dbf64f2e57bc457a63384424abd32defae2bbd1a Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Wed, 23 Nov 2022 20:20:43 -0800 Subject: [PATCH 6/7] simplify _copy_names, fix docstring errors, add names to sample_system and unit tests, namedio.copy is now deepcopy --- control/dtime.py | 29 ++++++++++++++++++++-- control/iosys.py | 31 +++++++++++++---------- control/namedio.py | 11 +++------ control/statesp.py | 29 +++++++++++----------- control/tests/discrete_test.py | 45 +++++++++++++++++++++++++++++++++- control/tests/iosys_test.py | 12 ++++----- control/tests/kwargs_test.py | 1 + control/xferfcn.py | 29 ++++++++++------------ 8 files changed, 127 insertions(+), 60 deletions(-) diff --git a/control/dtime.py b/control/dtime.py index b05d22b96..9dddd86a3 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -53,7 +53,8 @@ __all__ = ['sample_system', 'c2d'] # Sample a continuous time system -def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None): +def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None, + name=None, copy_names=True, **kwargs): """ Convert a continuous time system to discrete time by sampling @@ -72,12 +73,35 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None): prewarp_frequency : float within [0, infinity) The frequency [rad/s] at which to match with the input continuous- time system's magnitude and phase (only valid for method='bilinear') + name : string, optional + Set the name of the sampled system. If not specified and + if `copy_names` is `False`, a generic name is generated + with a unique integer id. If `copy_names` is `True`, the new system + name is determined by adding the prefix and suffix strings in + config.defaults['namedio.sampled_system_name_prefix'] and + config.defaults['namedio.sampled_system_name_suffix'], with the + default being to add the suffix '$sampled'. + copy_names : bool, Optional + If True, copy the names of the input signals, output + signals, and states to the sampled system. Returns ------- sysd : linsys Discrete time system, with sampling rate Ts + Additional Parameters + --------------------- + inputs : int, list of str or None, optional + Description of the system inputs. If not specified, the origional + system inputs are used. See :class:`NamedIOSystem` for more + information. + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. Only + available if the system is :class:`StateSpace`. + Notes ----- See :meth:`StateSpace.sample` or :meth:`TransferFunction.sample` for @@ -94,7 +118,8 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None): raise ValueError("First argument must be continuous time system") return sysc.sample(Ts, - method=method, alpha=alpha, prewarp_frequency=prewarp_frequency) + method=method, alpha=alpha, prewarp_frequency=prewarp_frequency, + name=name, copy_names=copy_names, **kwargs) def c2d(sysc, Ts, method='zoh', prewarp_frequency=None): diff --git a/control/iosys.py b/control/iosys.py index 5b941e964..b90d35ee3 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -568,16 +568,23 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6, StateSpace(A, B, C, D, self.dt, remove_useless_states=False)) # Set the system name, inputs, outputs, and states + if copy in kwargs: + copy_names = kwargs.pop('copy') + warn("keyword 'copy' is deprecated. please use 'copy_names'", + DeprecationWarning) + if copy_names: + linsys._copy_names(self) if name is None: - name = \ - config.defaults['namedio.linearized_system_name_prefix'] +\ - self.name + \ + linsys.name = \ + config.defaults['namedio.linearized_system_name_prefix']+\ + linsys.name+\ config.defaults['namedio.linearized_system_name_suffix'] - linsys._copy_names(self, name=name) - linsys = LinearIOSystem(linsys, name=name, **kwargs) - return linsys + else: + linsys.name = name + # re-init to include desired signal names if names were provided + return LinearIOSystem(linsys, **kwargs) class LinearIOSystem(InputOutputSystem, StateSpace): """Input/output representation of a linear (state space) system. @@ -2180,19 +2187,17 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw): params : dict, optional Parameter values for the systems. Passed to the evaluation functions for the system as default values, overriding internal defaults. - copy_names : bool, Optional - If `copy_names` is True, copy the names of the input signals, output - signals, and states to the linearized system. If `name` is not - specified, the system name is set to the input system name with the - string '_linearized' appended. name : string, optional Set the name of the linearized system. If not specified and - if `copy` is `False`, a generic name is generated - with a unique integer id. If `copy` is `True`, the new system + if `copy_names` is `False`, a generic name is generated + with a unique integer id. If `copy_names` is `True`, the new system name is determined by adding the prefix and suffix strings in config.defaults['namedio.linearized_system_name_prefix'] and config.defaults['namedio.linearized_system_name_suffix'], with the default being to add the suffix '$linearized'. + copy_names : bool, Optional + If True, Copy the names of the input signals, output signals, and + states to the linearized system. Returns ------- diff --git a/control/namedio.py b/control/namedio.py index 9f82e5929..a94d1a9f5 100644 --- a/control/namedio.py +++ b/control/namedio.py @@ -6,7 +6,7 @@ # and other similar classes to allow naming of signals. import numpy as np -from copy import copy +from copy import deepcopy from warnings import warn from . import config @@ -99,13 +99,10 @@ def __str__(self): def _find_signal(self, name, sigdict): return sigdict.get(name, None) - def _copy_names(self, sys, name=None): + def _copy_names(self, sys): """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. """ - if name is None: - self.name = sys.name - else: - self.name = name + self.name = sys.name self.ninputs, self.input_index = \ sys.ninputs, sys.input_index.copy() self.noutputs, self.output_index = \ @@ -126,7 +123,7 @@ def copy(self, name=None, use_prefix_suffix=True): """ # Create a copy of the system - newsys = copy(self) + newsys = deepcopy(self) # Update the system name if name is None and use_prefix_suffix: diff --git a/control/statesp.py b/control/statesp.py index 561b2d343..7843cb33f 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -357,7 +357,7 @@ def __init__(self, *args, init_namedio=True, **kwargs): states=states, dt=dt) elif kwargs: raise TypeError("unrecognized keyword(s): ", str(kwargs)) - + # Reset shapes (may not be needed once np.matrix support is removed) if self._isstatic(): # static gain @@ -1298,7 +1298,7 @@ def __getitem__(self, indices): return StateSpace(self.A, self.B[:, j], self.C[i, :], self.D[i, j], self.dt) - def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, + def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, name=None, copy_names=True, **kwargs): """Convert a continuous time system to discrete time @@ -1327,19 +1327,17 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, time system's magnitude and phase (the gain=1 crossover frequency, for example). Should only be specified with method='bilinear' or 'gbt' with alpha=0.5 and ignored otherwise. - copy_names : bool, Optional - If `copy_names` is True, copy the names of the input signals, output - signals, and states to the sampled system. If `name` is not - specified, the system name is set to the input system name with the - string '_sampled' appended. name : string, optional Set the name of the sampled system. If not specified and - if `copy` is `False`, a generic name is generated - with a unique integer id. If `copy` is `True`, the new system + if `copy_names` is `False`, a generic name is generated + with a unique integer id. If `copy_names` is `True`, the new system name is determined by adding the prefix and suffix strings in config.defaults['namedio.sampled_system_name_prefix'] and config.defaults['namedio.sampled_system_name_suffix'], with the default being to add the suffix '$sampled'. + copy_names : bool, Optional + If True, copy the names of the input signals, output + signals, and states to the sampled system. Returns ------- @@ -1380,15 +1378,16 @@ 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: - name = \ + sysd.name = \ config.defaults['namedio.sampled_system_name_prefix'] +\ - self.name + \ + sysd.name + \ config.defaults['namedio.sampled_system_name_suffix'] - sysd._copy_names(self, name=name) - # pass desired signal names if names were provided - sysd = StateSpace(sysd, name=name, **kwargs) - return sysd + else: + sysd.name = name + # pass desired signal names if names were provided + return StateSpace(sysd, **kwargs) def dcgain(self, warn_infinite=False): """Return the zero-frequency gain diff --git a/control/tests/discrete_test.py b/control/tests/discrete_test.py index 0842cbf59..4cf28a21b 100644 --- a/control/tests/discrete_test.py +++ b/control/tests/discrete_test.py @@ -458,4 +458,47 @@ def test_signal_names(self, tsys): assert ssd.state_labels == ['a', 'b', 'c'] assert ssd.output_labels == ['y'] assert tfd.input_labels == ['u'] - assert tfd.output_labels == ['y'] \ No newline at end of file + assert tfd.output_labels == ['y'] + + ssd = sample_system(ssc, 0.1) + tfd = sample_system(tfc, 0.1) + assert ssd.input_labels == ['u'] + assert ssd.state_labels == ['a', 'b', 'c'] + assert ssd.output_labels == ['y'] + assert tfd.input_labels == ['u'] + assert tfd.output_labels == ['y'] + + # system names and signal name override + sysc = StateSpace(1.1, 1, 1, 1, inputs='u', outputs='y', states='a') + + sysd = sample_system(sysc, 0.1, name='sampled') + assert sysd.name == 'sampled' + assert sysd.find_input('u') == 0 + assert sysd.find_output('y') == 0 + assert sysd.find_state('a') == 0 + + # If we copy signal names w/out a system name, append '$sampled' + sysd = sample_system(sysc, 0.1) + assert sysd.name == sysc.name + '$sampled' + + # If copy is False, signal names should not be copied + sysd_nocopy = sample_system(sysc, 0.1, copy_names=False) + assert sysd_nocopy.find_input('u') is None + assert sysd_nocopy.find_output('y') is None + assert sysd_nocopy.find_state('a') is None + + # if signal names are provided, they should override those of sysc + sysd_newnames = sample_system(sysc, 0.1, + inputs='v', outputs='x', states='b') + assert sysd_newnames.find_input('v') == 0 + assert sysd_newnames.find_input('u') is None + assert sysd_newnames.find_output('x') == 0 + assert sysd_newnames.find_output('y') is None + assert sysd_newnames.find_state('b') == 0 + assert sysd_newnames.find_state('a') is None + # test just one name + sysd_newnames = sample_system(sysc, 0.1, inputs='v') + assert sysd_newnames.find_input('v') == 0 + assert sysd_newnames.find_input('u') is None + assert sysd_newnames.find_output('y') == 0 + assert sysd_newnames.find_output('x') is None diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 6bed7cd16..cc9c3e721 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -231,12 +231,6 @@ def test_linearize_named_signals(self, kincar): linearized = kincar.linearize([0, 0, 0], [0, 0], copy_names=True) assert linearized.name == kincar.name + '$linearized' - # Test legacy version as well - ct.use_legacy_defaults('0.8.4') - ct.config.use_numpy_matrix(False) # np.matrix deprecated - linearized = kincar.linearize([0, 0, 0], [0, 0], copy_names=True) - assert linearized.name == kincar.name + '_linearized' - # If copy is False, signal names should not be copied lin_nocopy = kincar.linearize(0, 0, copy_names=False) assert lin_nocopy.find_input('v') is None @@ -257,6 +251,12 @@ def test_linearize_named_signals(self, kincar): assert linearized_newnames.find_output('x') is None assert linearized_newnames.find_output('y') is None + # Test legacy version as well + ct.use_legacy_defaults('0.8.4') + ct.config.use_numpy_matrix(False) # np.matrix deprecated + linearized = kincar.linearize([0, 0, 0], [0, 0], copy_names=True) + assert linearized.name == kincar.name + '_linearized' + def test_connect(self, tsys): # Define a couple of (linear) systems to interconnection linsys1 = tsys.siso_linsys diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 065c4673f..20a1c8e9c 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -183,6 +183,7 @@ def test_matplotlib_kwargs(function, nsysargs, moreargs, kwargs, mplcleanup): 'tf': test_unrecognized_kwargs, 'tf2io' : test_unrecognized_kwargs, 'tf2ss' : test_unrecognized_kwargs, + 'sample_system' : test_unrecognized_kwargs, 'flatsys.point_to_point': flatsys_test.TestFlatSys.test_point_to_point_errors, 'flatsys.solve_flat_ocp': diff --git a/control/xferfcn.py b/control/xferfcn.py index 79e46de7e..84188a63f 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1090,7 +1090,7 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): return num, den, denorder - def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, + def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, name=None, copy_names=True, **kwargs): """Convert a continuous-time system to discrete time @@ -1119,19 +1119,17 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, time system's magnitude and phase (the gain=1 crossover frequency, for example). Should only be specified with method='bilinear' or 'gbt' with alpha=0.5 and ignored otherwise. - copy_names : bool, Optional - If `copy_names` is True, copy the names of the input signals, output - signals, and states to the sampled system. If `name` is not - specified, the system name is set to the input system name with the - string '_sampled' appended. name : string, optional Set the name of the sampled system. If not specified and - if `copy` is `False`, a generic name is generated - with a unique integer id. If `copy` is `True`, the new system + if `copy_names` is `False`, a generic name is generated + with a unique integer id. If `copy_names` is `True`, the new system name is determined by adding the prefix and suffix strings in config.defaults['namedio.sampled_system_name_prefix'] and config.defaults['namedio.sampled_system_name_suffix'], with the default being to add the suffix '$sampled'. + copy_names : bool, Optional + If True, copy the names of the input signals, output + signals, and states to the sampled system. Returns ------- @@ -1146,8 +1144,6 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None, information. outputs : int, list of str or None, optional Description of the system outputs. Same format as `inputs`. - states : int, list of str, or None, optional - Description of the system states. Same format as `inputs`. Notes ----- @@ -1178,15 +1174,16 @@ 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: - name = \ + sysd.name = \ config.defaults['namedio.sampled_system_name_prefix'] +\ - self.name + \ + sysd.name + \ config.defaults['namedio.sampled_system_name_suffix'] - sysd._copy_names(self, name=name) - # pass desired signal names if names were provided - sysd = TransferFunction(sysd, name=name, **kwargs) - return sysd + else: + sysd.name = name + # pass desired signal names if names were provided + return TransferFunction(sysd, name=name, **kwargs) def dcgain(self, warn_infinite=False): """Return the zero-frequency (or DC) gain From 16d9e6af15ba245dc9964aa062e704bf833909ee Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Wed, 23 Nov 2022 20:39:35 -0800 Subject: [PATCH 7/7] fix deprecated functionality of copy keyword in iosys.linearize --- control/iosys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/iosys.py b/control/iosys.py index b90d35ee3..205bfe1f5 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -568,7 +568,7 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6, StateSpace(A, B, C, D, self.dt, remove_useless_states=False)) # Set the system name, inputs, outputs, and states - if copy in kwargs: + if 'copy' in kwargs: copy_names = kwargs.pop('copy') warn("keyword 'copy' is deprecated. please use 'copy_names'", DeprecationWarning)