Skip to content
Prev Previous commit
Next Next commit
simplify _copy_names, fix docstring errors, add names to sample_syste…
…m and unit tests, namedio.copy is now deepcopy
  • Loading branch information
sawyerbfuller committed Nov 24, 2022
commit dbf64f2e57bc457a63384424abd32defae2bbd1a
29 changes: 27 additions & 2 deletions control/dtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 <sys[id]> 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
Expand All @@ -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):
Expand Down
31 changes: 18 additions & 13 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <sys[id]> is generated
with a unique integer id. If `copy` is `True`, the new system
if `copy_names` is `False`, a generic name <sys[id]> 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
-------
Expand Down
11 changes: 4 additions & 7 deletions control/namedio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 = \
Expand All @@ -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:
Expand Down
29 changes: 14 additions & 15 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 <sys[id]> is generated
with a unique integer id. If `copy` is `True`, the new system
if `copy_names` is `False`, a generic name <sys[id]> 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
-------
Expand Down Expand Up @@ -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
Expand Down
45 changes: 44 additions & 1 deletion control/tests/discrete_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
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
12 changes: 6 additions & 6 deletions control/tests/iosys_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions control/tests/kwargs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
29 changes: 13 additions & 16 deletions control/xferfcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <sys[id]> is generated
with a unique integer id. If `copy` is `True`, the new system
if `copy_names` is `False`, a generic name <sys[id]> 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
-------
Expand All @@ -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
-----
Expand Down Expand Up @@ -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
Expand Down