Skip to content

Commit ed4ff84

Browse files
authored
Merge pull request python-control#797 from sawyerbfuller/named-signals
Preserve signal names upon conversion to discrete-time
2 parents 23a7791 + 16d9e6a commit ed4ff84

File tree

11 files changed

+319
-69
lines changed

11 files changed

+319
-69
lines changed

control/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ def reset_defaults():
9797
from .rlocus import _rlocus_defaults
9898
defaults.update(_rlocus_defaults)
9999

100+
from .namedio import _namedio_defaults
101+
defaults.update(_namedio_defaults)
102+
100103
from .xferfcn import _xferfcn_defaults
101104
defaults.update(_xferfcn_defaults)
102105

@@ -285,7 +288,7 @@ def use_legacy_defaults(version):
285288
set_defaults('control', default_dt=None)
286289

287290
# changed iosys naming conventions
288-
set_defaults('iosys', state_name_delim='.',
291+
set_defaults('namedio', state_name_delim='.',
289292
duplicate_system_name_prefix='copy of ',
290293
duplicate_system_name_suffix='',
291294
linearized_system_name_prefix='',

control/dtime.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
__all__ = ['sample_system', 'c2d']
5454

5555
# Sample a continuous time system
56-
def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None):
56+
def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None,
57+
name=None, copy_names=True, **kwargs):
5758
"""
5859
Convert a continuous time system to discrete time by sampling
5960
@@ -72,12 +73,35 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None):
7273
prewarp_frequency : float within [0, infinity)
7374
The frequency [rad/s] at which to match with the input continuous-
7475
time system's magnitude and phase (only valid for method='bilinear')
76+
name : string, optional
77+
Set the name of the sampled system. If not specified and
78+
if `copy_names` is `False`, a generic name <sys[id]> is generated
79+
with a unique integer id. If `copy_names` is `True`, the new system
80+
name is determined by adding the prefix and suffix strings in
81+
config.defaults['namedio.sampled_system_name_prefix'] and
82+
config.defaults['namedio.sampled_system_name_suffix'], with the
83+
default being to add the suffix '$sampled'.
84+
copy_names : bool, Optional
85+
If True, copy the names of the input signals, output
86+
signals, and states to the sampled system.
7587
7688
Returns
7789
-------
7890
sysd : linsys
7991
Discrete time system, with sampling rate Ts
8092
93+
Additional Parameters
94+
---------------------
95+
inputs : int, list of str or None, optional
96+
Description of the system inputs. If not specified, the origional
97+
system inputs are used. See :class:`NamedIOSystem` for more
98+
information.
99+
outputs : int, list of str or None, optional
100+
Description of the system outputs. Same format as `inputs`.
101+
states : int, list of str, or None, optional
102+
Description of the system states. Same format as `inputs`. Only
103+
available if the system is :class:`StateSpace`.
104+
81105
Notes
82106
-----
83107
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):
94118
raise ValueError("First argument must be continuous time system")
95119

96120
return sysc.sample(Ts,
97-
method=method, alpha=alpha, prewarp_frequency=prewarp_frequency)
121+
method=method, alpha=alpha, prewarp_frequency=prewarp_frequency,
122+
name=name, copy_names=copy_names, **kwargs)
98123

99124

100125
def c2d(sysc, Ts, method='zoh', prewarp_frequency=None):

control/iosys.py

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,7 @@
4747
'interconnect', 'summing_junction']
4848

4949
# Define module default parameter values
50-
_iosys_defaults = {
51-
'iosys.state_name_delim': '_',
52-
'iosys.duplicate_system_name_prefix': '',
53-
'iosys.duplicate_system_name_suffix': '$copy',
54-
'iosys.linearized_system_name_prefix': '',
55-
'iosys.linearized_system_name_suffix': '$linearized'
56-
}
50+
_iosys_defaults = {}
5751

5852

5953
class InputOutputSystem(NamedIOSystem):
@@ -515,7 +509,7 @@ def feedback(self, other=1, sign=-1, params=None):
515509
return newsys
516510

517511
def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
518-
name=None, copy=False, **kwargs):
512+
name=None, copy_names=False, **kwargs):
519513
"""Linearize an input/output system at a given state and input.
520514
521515
Return the linearization of an input/output system at a given state
@@ -571,25 +565,26 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
571565

572566
# Create the state space system
573567
linsys = LinearIOSystem(
574-
StateSpace(A, B, C, D, self.dt, remove_useless_states=False),
575-
name=name, **kwargs)
568+
StateSpace(A, B, C, D, self.dt, remove_useless_states=False))
576569

577-
# Set the names the system, inputs, outputs, and states
578-
if copy:
570+
# Set the system name, inputs, outputs, and states
571+
if 'copy' in kwargs:
572+
copy_names = kwargs.pop('copy')
573+
warn("keyword 'copy' is deprecated. please use 'copy_names'",
574+
DeprecationWarning)
575+
576+
if copy_names:
577+
linsys._copy_names(self)
579578
if name is None:
580579
linsys.name = \
581-
config.defaults['iosys.linearized_system_name_prefix'] + \
582-
self.name + \
583-
config.defaults['iosys.linearized_system_name_suffix']
584-
linsys.ninputs, linsys.input_index = self.ninputs, \
585-
self.input_index.copy()
586-
linsys.noutputs, linsys.output_index = \
587-
self.noutputs, self.output_index.copy()
588-
linsys.nstates, linsys.state_index = \
589-
self.nstates, self.state_index.copy()
590-
591-
return linsys
580+
config.defaults['namedio.linearized_system_name_prefix']+\
581+
linsys.name+\
582+
config.defaults['namedio.linearized_system_name_suffix']
583+
else:
584+
linsys.name = name
592585

586+
# re-init to include desired signal names if names were provided
587+
return LinearIOSystem(linsys, **kwargs)
593588

594589
class LinearIOSystem(InputOutputSystem, StateSpace):
595590
"""Input/output representation of a linear (state space) system.
@@ -966,7 +961,7 @@ def __init__(self, syslist, connections=None, inplist=None, outlist=None,
966961

967962
if states is None:
968963
states = []
969-
state_name_delim = config.defaults['iosys.state_name_delim']
964+
state_name_delim = config.defaults['namedio.state_name_delim']
970965
for sys, sysname in sysobj_name_dct.items():
971966
states += [sysname + state_name_delim +
972967
statename for statename in sys.state_index.keys()]
@@ -2197,19 +2192,17 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw):
21972192
params : dict, optional
21982193
Parameter values for the systems. Passed to the evaluation functions
21992194
for the system as default values, overriding internal defaults.
2200-
copy : bool, Optional
2201-
If `copy` is True, copy the names of the input signals, output signals,
2202-
and states to the linearized system. If `name` is not specified,
2203-
the system name is set to the input system name with the string
2204-
'_linearized' appended.
22052195
name : string, optional
22062196
Set the name of the linearized system. If not specified and
2207-
if `copy` is `False`, a generic name <sys[id]> is generated
2208-
with a unique integer id. If `copy` is `True`, the new system
2197+
if `copy_names` is `False`, a generic name <sys[id]> is generated
2198+
with a unique integer id. If `copy_names` is `True`, the new system
22092199
name is determined by adding the prefix and suffix strings in
2210-
config.defaults['iosys.linearized_system_name_prefix'] and
2211-
config.defaults['iosys.linearized_system_name_suffix'], with the
2200+
config.defaults['namedio.linearized_system_name_prefix'] and
2201+
config.defaults['namedio.linearized_system_name_suffix'], with the
22122202
default being to add the suffix '$linearized'.
2203+
copy_names : bool, Optional
2204+
If True, Copy the names of the input signals, output signals, and
2205+
states to the linearized system.
22132206
22142207
Returns
22152208
-------
@@ -2221,7 +2214,7 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw):
22212214
---------------------
22222215
inputs : int, list of str or None, optional
22232216
Description of the system inputs. If not specified, the origional
2224-
system inputs are used. See :class:`InputOutputSystem` for more
2217+
system inputs are used. See :class:`NamedIOSystem` for more
22252218
information.
22262219
outputs : int, list of str or None, optional
22272220
Description of the system outputs. Same format as `inputs`.
@@ -2733,8 +2726,8 @@ def interconnect(syslist, connections=None, inplist=None, outlist=None,
27332726
If a system is duplicated in the list of systems to be connected,
27342727
a warning is generated and a copy of the system is created with the
27352728
name of the new system determined by adding the prefix and suffix
2736-
strings in config.defaults['iosys.linearized_system_name_prefix']
2737-
and config.defaults['iosys.linearized_system_name_suffix'], with the
2729+
strings in config.defaults['namedio.linearized_system_name_prefix']
2730+
and config.defaults['namedio.linearized_system_name_suffix'], with the
27382731
default being to add the suffix '$copy'$ to the system name.
27392732
27402733
It is possible to replace lists in most of arguments with tuples instead,

control/namedio.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,24 @@
66
# and other similar classes to allow naming of signals.
77

88
import numpy as np
9-
from copy import copy
9+
from copy import deepcopy
1010
from warnings import warn
1111
from . import config
1212

1313
__all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual',
1414
'isdtime', 'isctime']
15-
15+
# Define module default parameter values
16+
_namedio_defaults = {
17+
'namedio.state_name_delim': '_',
18+
'namedio.duplicate_system_name_prefix': '',
19+
'namedio.duplicate_system_name_suffix': '$copy',
20+
'namedio.linearized_system_name_prefix': '',
21+
'namedio.linearized_system_name_suffix': '$linearized',
22+
'namedio.sampled_system_name_prefix': '',
23+
'namedio.sampled_system_name_suffix': '$sampled'
24+
}
25+
26+
1627
class NamedIOSystem(object):
1728
def __init__(
1829
self, name=None, inputs=None, outputs=None, states=None, **kwargs):
@@ -88,26 +99,37 @@ def __str__(self):
8899
def _find_signal(self, name, sigdict):
89100
return sigdict.get(name, None)
90101

102+
def _copy_names(self, sys):
103+
"""copy the signal and system name of sys. Name is given as a keyword
104+
in case a specific name (e.g. append 'linearized') is desired. """
105+
self.name = sys.name
106+
self.ninputs, self.input_index = \
107+
sys.ninputs, sys.input_index.copy()
108+
self.noutputs, self.output_index = \
109+
sys.noutputs, sys.output_index.copy()
110+
self.nstates, self.state_index = \
111+
sys.nstates, sys.state_index.copy()
112+
91113
def copy(self, name=None, use_prefix_suffix=True):
92114
"""Make a copy of an input/output system
93115
94116
A copy of the system is made, with a new name. The `name` keyword
95117
can be used to specify a specific name for the system. If no name
96118
is given and `use_prefix_suffix` is True, the name is constructed
97-
by prepending config.defaults['iosys.duplicate_system_name_prefix']
98-
and appending config.defaults['iosys.duplicate_system_name_suffix'].
119+
by prepending config.defaults['namedio.duplicate_system_name_prefix']
120+
and appending config.defaults['namedio.duplicate_system_name_suffix'].
99121
Otherwise, a generic system name of the form `sys[<id>]` is used,
100122
where `<id>` is based on an internal counter.
101123
102124
"""
103125
# Create a copy of the system
104-
newsys = copy(self)
126+
newsys = deepcopy(self)
105127

106128
# Update the system name
107129
if name is None and use_prefix_suffix:
108130
# Get the default prefix and suffix to use
109-
dup_prefix = config.defaults['iosys.duplicate_system_name_prefix']
110-
dup_suffix = config.defaults['iosys.duplicate_system_name_suffix']
131+
dup_prefix = config.defaults['namedio.duplicate_system_name_prefix']
132+
dup_suffix = config.defaults['namedio.duplicate_system_name_suffix']
111133
newsys.name = self._name_or_default(
112134
dup_prefix + self.name + dup_suffix)
113135
else:

control/statesp.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@
6363
from .exception import ControlSlycot
6464
from .frdata import FrequencyResponseData
6565
from .lti import LTI, _process_frequency_response
66-
from .namedio import common_timebase, isdtime
67-
from .namedio import _process_namedio_keywords
66+
from .namedio import common_timebase, isdtime, _process_namedio_keywords, \
67+
_process_dt_keyword
6868
from . import config
6969
from copy import deepcopy
7070

@@ -359,7 +359,7 @@ def __init__(self, *args, init_namedio=True, **kwargs):
359359
raise TypeError("unrecognized keyword(s): ", str(kwargs))
360360

361361
# Reset shapes (may not be needed once np.matrix support is removed)
362-
if 0 == self.nstates:
362+
if self._isstatic():
363363
# static gain
364364
# matrix's default "empty" shape is 1x0
365365
A.shape = (0, 0)
@@ -1298,7 +1298,8 @@ def __getitem__(self, indices):
12981298
return StateSpace(self.A, self.B[:, j], self.C[i, :],
12991299
self.D[i, j], self.dt)
13001300

1301-
def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None):
1301+
def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None,
1302+
name=None, copy_names=True, **kwargs):
13021303
"""Convert a continuous time system to discrete time
13031304
13041305
Creates a discrete-time system from a continuous-time system by
@@ -1317,22 +1318,42 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None):
13171318
alpha=0)
13181319
* backward_diff: Backwards differencing ("gbt" with alpha=1.0)
13191320
* zoh: zero-order hold (default)
1320-
13211321
alpha : float within [0, 1]
13221322
The generalized bilinear transformation weighting parameter, which
13231323
should only be specified with method="gbt", and is ignored
13241324
otherwise
1325-
13261325
prewarp_frequency : float within [0, infinity)
13271326
The frequency [rad/s] at which to match with the input continuous-
13281327
time system's magnitude and phase (the gain=1 crossover frequency,
13291328
for example). Should only be specified with method='bilinear' or
13301329
'gbt' with alpha=0.5 and ignored otherwise.
1330+
name : string, optional
1331+
Set the name of the sampled system. If not specified and
1332+
if `copy_names` is `False`, a generic name <sys[id]> is generated
1333+
with a unique integer id. If `copy_names` is `True`, the new system
1334+
name is determined by adding the prefix and suffix strings in
1335+
config.defaults['namedio.sampled_system_name_prefix'] and
1336+
config.defaults['namedio.sampled_system_name_suffix'], with the
1337+
default being to add the suffix '$sampled'.
1338+
copy_names : bool, Optional
1339+
If True, copy the names of the input signals, output
1340+
signals, and states to the sampled system.
13311341
13321342
Returns
13331343
-------
13341344
sysd : StateSpace
1335-
Discrete time system, with sampling rate Ts
1345+
Discrete-time system, with sampling rate Ts
1346+
1347+
Additional Parameters
1348+
---------------------
1349+
inputs : int, list of str or None, optional
1350+
Description of the system inputs. If not specified, the origional
1351+
system inputs are used. See :class:`InputOutputSystem` for more
1352+
information.
1353+
outputs : int, list of str or None, optional
1354+
Description of the system outputs. Same format as `inputs`.
1355+
states : int, list of str, or None, optional
1356+
Description of the system states. Same format as `inputs`.
13361357
13371358
Notes
13381359
-----
@@ -1347,14 +1368,26 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None):
13471368
if not self.isctime():
13481369
raise ValueError("System must be continuous time system")
13491370

1350-
sys = (self.A, self.B, self.C, self.D)
13511371
if (method == 'bilinear' or (method == 'gbt' and alpha == 0.5)) and \
13521372
prewarp_frequency is not None:
13531373
Twarp = 2 * np.tan(prewarp_frequency * Ts/2)/prewarp_frequency
13541374
else:
13551375
Twarp = Ts
1376+
sys = (self.A, self.B, self.C, self.D)
13561377
Ad, Bd, C, D, _ = cont2discrete(sys, Twarp, method, alpha)
1357-
return StateSpace(Ad, Bd, C, D, Ts)
1378+
sysd = StateSpace(Ad, Bd, C, D, Ts)
1379+
# copy over the system name, inputs, outputs, and states
1380+
if copy_names:
1381+
sysd._copy_names(self)
1382+
if name is None:
1383+
sysd.name = \
1384+
config.defaults['namedio.sampled_system_name_prefix'] +\
1385+
sysd.name + \
1386+
config.defaults['namedio.sampled_system_name_suffix']
1387+
else:
1388+
sysd.name = name
1389+
# pass desired signal names if names were provided
1390+
return StateSpace(sysd, **kwargs)
13581391

13591392
def dcgain(self, warn_infinite=False):
13601393
"""Return the zero-frequency gain

0 commit comments

Comments
 (0)