Skip to content

Preserve signal names upon conversion to discrete-time #797

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 26, 2022
5 changes: 4 additions & 1 deletion control/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -285,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='',
Expand Down
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
65 changes: 29 additions & 36 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -571,25 +565,26 @@ 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 names the system, inputs, outputs, and states
if copy:
# 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:
linsys.name = \
config.defaults['iosys.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()

return linsys
config.defaults['namedio.linearized_system_name_prefix']+\
linsys.name+\
config.defaults['namedio.linearized_system_name_suffix']
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 @@ -966,7 +961,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()]
Expand Down Expand Up @@ -2192,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 : 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.
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['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'.
copy_names : bool, Optional
If True, Copy the names of the input signals, output signals, and
states to the linearized system.

Returns
-------
Expand All @@ -2216,7 +2209,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`.
Expand Down Expand Up @@ -2728,8 +2721,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,
Expand Down
36 changes: 29 additions & 7 deletions control/namedio.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@
# 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

__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):
Expand Down Expand Up @@ -88,26 +99,37 @@ def __str__(self):
def _find_signal(self, name, sigdict):
return sigdict.get(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. """
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[<id>]` is used,
where `<id>` is based on an internal counter.

"""
# Create a copy of the system
newsys = copy(self)
newsys = deepcopy(self)

# 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:
Expand Down
51 changes: 42 additions & 9 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -359,7 +359,7 @@ def __init__(self, *args, init_namedio=True, **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)
Expand Down Expand Up @@ -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
Expand All @@ -1317,22 +1318,42 @@ 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.
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 : 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
-----
Expand All @@ -1347,14 +1368,26 @@ 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)
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.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
Loading