Skip to content

Change name of converted LinearIOSystems #903

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 5 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,13 +591,8 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
DeprecationWarning)

if copy_names:
linsys._copy_names(self)
if name is None:
linsys.name = \
config.defaults['namedio.linearized_system_name_prefix']+\
linsys.name+\
config.defaults['namedio.linearized_system_name_suffix']
else:
linsys._copy_names(self, prefix_suffix_name='linearized')
if name is not None:
linsys.name = name

# re-init to include desired signal names if names were provided
Expand Down Expand Up @@ -2400,7 +2395,9 @@ def ss(*args, **kwargs):
"non-unique state space realization")

# Create a state space system from an LTI system
sys = LinearIOSystem(_convert_to_statespace(sys), **kwargs)
sys = LinearIOSystem(
_convert_to_statespace(sys, use_prefix_suffix=True), **kwargs)

else:
raise TypeError("ss(sys): sys must be a StateSpace or "
"TransferFunction object. It is %s." % type(sys))
Expand Down
41 changes: 27 additions & 14 deletions control/namedio.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

__all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual',
'isdtime', 'isctime']

# Define module default parameter values
_namedio_defaults = {
'namedio.state_name_delim': '_',
Expand All @@ -20,7 +21,9 @@
'namedio.linearized_system_name_prefix': '',
'namedio.linearized_system_name_suffix': '$linearized',
'namedio.sampled_system_name_prefix': '',
'namedio.sampled_system_name_suffix': '$sampled'
'namedio.sampled_system_name_suffix': '$sampled',
'namedio.converted_system_name_prefix': '',
'namedio.converted_system_name_suffix': '$converted',
}


Expand Down Expand Up @@ -49,11 +52,15 @@ def __init__(
_idCounter = 0 # Counter for creating generic system name

# Return system name
def _name_or_default(self, name=None):
def _name_or_default(self, name=None, prefix_suffix_name=None):
if name is None:
name = "sys[{}]".format(NamedIOSystem._idCounter)
NamedIOSystem._idCounter += 1
return name
prefix = "" if prefix_suffix_name is None else config.defaults[
'namedio.' + prefix_suffix_name + '_system_name_prefix']
suffix = "" if prefix_suffix_name is None else config.defaults[
'namedio.' + prefix_suffix_name + '_system_name_suffix']
return prefix + name + suffix

# Check if system name is generic
def _generic_name_check(self):
Expand Down Expand Up @@ -99,16 +106,24 @@ def __str__(self):
def _find_signal(self, name, sigdict):
return sigdict.get(name, None)

def _copy_names(self, sys):
def _copy_names(self, sys, prefix="", suffix="", prefix_suffix_name=None):
"""copy the signal and system name of sys. Name is given as a keyword
in case a specific name (e.g. append 'linearized') is desired. """
self.name = sys.name
self.ninputs, self.input_index = \
sys.ninputs, sys.input_index.copy()
self.noutputs, self.output_index = \
sys.noutputs, sys.output_index.copy()
self.nstates, self.state_index = \
sys.nstates, sys.state_index.copy()
# Figure out the system name and assign it
if prefix == "" and prefix_suffix_name is not None:
prefix = config.defaults[
'namedio.' + prefix_suffix_name + '_system_name_prefix']
if suffix == "" and prefix_suffix_name is not None:
suffix = config.defaults[
'namedio.' + prefix_suffix_name + '_system_name_suffix']
self.name = prefix + sys.name + suffix

# Name the inputs, outputs, and states
self.input_index = sys.input_index.copy()
self.output_index = sys.output_index.copy()
if self.nstates and sys.nstates:
# only copy state names for state space systems
self.state_index = sys.state_index.copy()

def copy(self, name=None, use_prefix_suffix=True):
"""Make a copy of an input/output system
Expand All @@ -128,10 +143,8 @@ def copy(self, name=None, use_prefix_suffix=True):
# Update the system name
if name is None and use_prefix_suffix:
# Get the default prefix and suffix to use
dup_prefix = config.defaults['namedio.duplicate_system_name_prefix']
dup_suffix = config.defaults['namedio.duplicate_system_name_suffix']
newsys.name = self._name_or_default(
dup_prefix + self.name + dup_suffix)
self.name, prefix_suffix_name='duplicate')
else:
newsys.name = self._name_or_default(name)

Expand Down
36 changes: 16 additions & 20 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1382,13 +1382,8 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None,
sysd = StateSpace(Ad, Bd, C, D, Ts)
# copy over the system name, inputs, outputs, and states
if copy_names:
sysd._copy_names(self)
if name is None:
sysd.name = \
config.defaults['namedio.sampled_system_name_prefix'] +\
sysd.name + \
config.defaults['namedio.sampled_system_name_suffix']
else:
sysd._copy_names(self, prefix_suffix_name='sampled')
if name is not None:
sysd.name = name
# pass desired signal names if names were provided
return StateSpace(sysd, **kwargs)
Expand Down Expand Up @@ -1524,7 +1519,7 @@ def output(self, t, x, u=None, params=None):


# TODO: add discrete time check
def _convert_to_statespace(sys):
def _convert_to_statespace(sys, use_prefix_suffix=False):
"""Convert a system to state space form (if needed).

If sys is already a state space, then it is returned. If sys is a
Expand Down Expand Up @@ -1561,11 +1556,10 @@ def _convert_to_statespace(sys):
denorder, den, num, tol=0)

states = ssout[0]
return StateSpace(
newsys = StateSpace(
ssout[1][:states, :states], ssout[2][:states, :sys.ninputs],
ssout[3][:sys.noutputs, :states], ssout[4], sys.dt,
inputs=sys.input_labels, outputs=sys.output_labels,
name=sys.name)
ssout[3][:sys.noutputs, :states], ssout[4], sys.dt)

except ImportError:
# No Slycot. Scipy tf->ss can't handle MIMO, but static
# MIMO is an easy special case we can check for here
Expand All @@ -1578,9 +1572,7 @@ def _convert_to_statespace(sys):
for i, j in itertools.product(range(sys.noutputs),
range(sys.ninputs)):
D[i, j] = sys.num[i][j][0] / sys.den[i][j][0]
return StateSpace([], [], [], D, sys.dt,
inputs=sys.input_labels, outputs=sys.output_labels,
name=sys.name)
newsys = StateSpace([], [], [], D, sys.dt)
else:
if sys.ninputs != 1 or sys.noutputs != 1:
raise TypeError("No support for MIMO without slycot")
Expand All @@ -1590,9 +1582,13 @@ def _convert_to_statespace(sys):
# the squeeze
A, B, C, D = \
sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den))
return StateSpace(
A, B, C, D, sys.dt, inputs=sys.input_labels,
outputs=sys.output_labels, name=sys.name)
newsys = StateSpace(A, B, C, D, sys.dt)

# Copy over the signal (and system) names
newsys._copy_names(
sys,
prefix_suffix_name='converted' if use_prefix_suffix else None)
return newsys

elif isinstance(sys, FrequencyResponseData):
raise TypeError("Can't convert FRD to StateSpace system.")
Expand All @@ -1605,7 +1601,6 @@ def _convert_to_statespace(sys):
except Exception:
raise TypeError("Can't convert given type to StateSpace system.")


# TODO: add discrete time option
def _rss_generate(
states, inputs, outputs, cdtype, strictly_proper=False, name=None):
Expand Down Expand Up @@ -1919,7 +1914,8 @@ def tf2ss(*args, **kwargs):
if not isinstance(sys, TransferFunction):
raise TypeError("tf2ss(sys): sys must be a TransferFunction "
"object.")
return StateSpace(_convert_to_statespace(sys), **kwargs)
return StateSpace(_convert_to_statespace(sys, use_prefix_suffix=True),
**kwargs)
else:
raise ValueError("Needs 1 or 2 arguments; received %i." % len(args))

Expand Down
67 changes: 49 additions & 18 deletions control/tests/interconnect_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,23 +246,54 @@ def test_string_inputoutput():
assert P_s2.output_index == {'y2' : 0}

def test_linear_interconnect():
tf_ctrl = ct.tf(1, (10.1, 1), inputs='e', outputs='u')
tf_plant = ct.tf(1, (10.1, 1), inputs='u', outputs='y')
ss_ctrl = ct.ss(1, 2, 1, 2, inputs='e', outputs='u')
ss_plant = ct.ss(1, 2, 1, 2, inputs='u', outputs='y')
tf_ctrl = ct.tf(1, (10.1, 1), inputs='e', outputs='u', name='ctrl')
tf_plant = ct.tf(1, (10.1, 1), inputs='u', outputs='y', name='plant')
ss_ctrl = ct.ss(1, 2, 1, 0, inputs='e', outputs='u', name='ctrl')
ss_plant = ct.ss(1, 2, 1, 0, inputs='u', outputs='y', name='plant')
nl_ctrl = ct.NonlinearIOSystem(
lambda t, x, u, params: x*x,
lambda t, x, u, params: u*x, states=1, inputs='e', outputs='u')
lambda t, x, u, params: x*x, lambda t, x, u, params: u*x,
states=1, inputs='e', outputs='u', name='ctrl')
nl_plant = ct.NonlinearIOSystem(
lambda t, x, u, params: x*x,
lambda t, x, u, params: u*x, states=1, inputs='u', outputs='y')

assert isinstance(ct.interconnect((tf_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert isinstance(ct.interconnect((ss_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert isinstance(ct.interconnect((tf_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert isinstance(ct.interconnect((ss_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem)

assert ~isinstance(ct.interconnect((nl_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert ~isinstance(ct.interconnect((nl_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert ~isinstance(ct.interconnect((ss_ctrl, nl_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert ~isinstance(ct.interconnect((tf_ctrl, nl_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
lambda t, x, u, params: x*x, lambda t, x, u, params: u*x,
states=1, inputs='u', outputs='y', name='plant')
sumblk = ct.summing_junction(inputs=['r', '-y'], outputs=['e'], name='sum')

# Interconnections of linear I/O systems should be linear I/O system
assert isinstance(
ct.interconnect([tf_ctrl, tf_plant, sumblk], inputs='r', outputs='y'),
ct.LinearIOSystem)
assert isinstance(
ct.interconnect([ss_ctrl, ss_plant, sumblk], inputs='r', outputs='y'),
ct.LinearIOSystem)
assert isinstance(
ct.interconnect([tf_ctrl, ss_plant, sumblk], inputs='r', outputs='y'),
ct.LinearIOSystem)
assert isinstance(
ct.interconnect([ss_ctrl, tf_plant, sumblk], inputs='r', outputs='y'),
ct.LinearIOSystem)

# Interconnections with nonliner I/O systems should not be linear
assert ~isinstance(
ct.interconnect([nl_ctrl, ss_plant, sumblk], inputs='r', outputs='y'),
ct.LinearIOSystem)
assert ~isinstance(
ct.interconnect([nl_ctrl, tf_plant, sumblk], inputs='r', outputs='y'),
ct.LinearIOSystem)
assert ~isinstance(
ct.interconnect([ss_ctrl, nl_plant, sumblk], inputs='r', outputs='y'),
ct.LinearIOSystem)
assert ~isinstance(
ct.interconnect([tf_ctrl, nl_plant, sumblk], inputs='r', outputs='y'),
ct.LinearIOSystem)

# Implicit converstion of transfer function should retain name
clsys = ct.interconnect(
[tf_ctrl, ss_plant, sumblk],
connections=[
['plant.u', 'ctrl.u'],
['ctrl.e', 'sum.e'],
['sum.y', 'plant.y']
],
inplist=['sum.r'], inputs='r',
outlist=['plant.y'], outputs='y')
assert clsys.syslist[0].name == 'ctrl'
7 changes: 5 additions & 2 deletions control/tests/namedio_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ def test_io_naming(fun, args, kwargs):
assert sys_ss != sys_r
assert sys_ss.input_labels == input_labels
assert sys_ss.output_labels == output_labels
if not isinstance(sys_r, ct.StateSpace):
# System should get unique name
assert sys_ss.name != sys_r.name

# Reassign system and signal names
sys_ss = ct.ss(
Expand Down Expand Up @@ -247,11 +250,11 @@ def test_convert_to_statespace():

# check that name, inputs, and outputs passed through
sys_new = ct.ss(sys)
assert sys_new.name == 'sys'
assert sys_new.name == 'sys$converted'
assert sys_new.input_labels == ['u']
assert sys_new.output_labels == ['y']
sys_new = ct.ss(sys_static)
assert sys_new.name == 'sys_static'
assert sys_new.name == 'sys_static$converted'
assert sys_new.input_labels == ['u']
assert sys_new.output_labels == ['y']

Expand Down
6 changes: 6 additions & 0 deletions control/tests/xferfcn_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,12 @@ def test_copy_names(create, args, kwargs, convert):
if cpy.nstates is not None and sys.nstates is not None:
assert cpy.state_labels == sys.state_labels

# Make sure that names aren't the same if system changed type
if not isinstance(cpy, create):
assert cpy.name == sys.name + '$converted'
else:
assert cpy.name == sys.name

# Relabel inputs and outputs
cpy = convert(sys, inputs='myin', outputs='myout')
assert cpy.input_labels == ['myin']
Expand Down
20 changes: 10 additions & 10 deletions control/xferfcn.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,13 +1206,8 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None,
sysd = TransferFunction(numd[0, :], dend, Ts)
# copy over the system name, inputs, outputs, and states
if copy_names:
sysd._copy_names(self)
if name is None:
sysd.name = \
config.defaults['namedio.sampled_system_name_prefix'] +\
sysd.name + \
config.defaults['namedio.sampled_system_name_suffix']
else:
sysd._copy_names(self, prefix_suffix_name='sampled')
if name is not None:
sysd.name = name
# pass desired signal names if names were provided
return TransferFunction(sysd, name=name, **kwargs)
Expand Down Expand Up @@ -1438,7 +1433,8 @@ def _add_siso(num1, den1, num2, den2):
return num, den


def _convert_to_transfer_function(sys, inputs=1, outputs=1):
def _convert_to_transfer_function(
sys, inputs=1, outputs=1, use_prefix_suffix=False):
"""Convert a system to transfer function form (if needed).

If sys is already a transfer function, then it is returned. If sys is a
Expand Down Expand Up @@ -1514,7 +1510,10 @@ def _convert_to_transfer_function(sys, inputs=1, outputs=1):
num = squeeze(num) # Convert to 1D array
den = squeeze(den) # Probably not needed

return TransferFunction(num, den, sys.dt)
newsys = TransferFunction(num, den, sys.dt)
if use_prefix_suffix:
newsys._copy_names(sys, prefix_suffix_name='converted')
return newsys

elif isinstance(sys, (int, float, complex, np.number)):
num = [[[sys] for j in range(inputs)] for i in range(outputs)]
Expand Down Expand Up @@ -1814,7 +1813,8 @@ def ss2tf(*args, **kwargs):
if not kwargs.get('outputs'):
kwargs['outputs'] = sys.output_labels
return TransferFunction(
_convert_to_transfer_function(sys), **kwargs)
_convert_to_transfer_function(sys, use_prefix_suffix=True),
**kwargs)
else:
raise TypeError(
"ss2tf(sys): sys must be a StateSpace object. It is %s."
Expand Down