Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
ss -> nlsys with name preservation/override + docstring update
  • Loading branch information
murrayrm committed Dec 20, 2024
commit 1547326d53782405177294e3ace99769967a7b75
20 changes: 13 additions & 7 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,8 @@ 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. """
# Figure out the system name and assign it
if prefix == "" and prefix_suffix_name is not None:
prefix = config.defaults[
'iosys.' + prefix_suffix_name + '_system_name_prefix']
if suffix == "" and prefix_suffix_name is not None:
suffix = config.defaults[
'iosys.' + prefix_suffix_name + '_system_name_suffix']
self.name = prefix + sys.name + suffix
self.name = _extended_system_name(
sys.name, prefix, suffix, prefix_suffix_name)

# Name the inputs, outputs, and states
self.input_index = sys.input_index.copy()
Expand Down Expand Up @@ -1058,3 +1053,14 @@ def _process_subsys_index(idx, sys_labels, slice_to_list=False):
idx = range(len(sys_labels))[idx]

return idx, labels


# Create an extended system name
def _extended_system_name(name, prefix="", suffix="", prefix_suffix_name=None):
if prefix == "" and prefix_suffix_name is not None:
prefix = config.defaults[
'iosys.' + prefix_suffix_name + '_system_name_prefix']
if suffix == "" and prefix_suffix_name is not None:
suffix = config.defaults[
'iosys.' + prefix_suffix_name + '_system_name_suffix']
return prefix + name + suffix
78 changes: 50 additions & 28 deletions control/nlsys.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@
class NonlinearIOSystem(InputOutputSystem):
"""Nonlinear I/O system.

Creates an :class:`~control.InputOutputSystem` for a nonlinear system by
specifying a state update function and an output function. The new system
can be a continuous or discrete time system (Note: discrete-time systems
are not yet supported by most functions.)
Creates an :class:`~control.InputOutputSystem` for a nonlinear system
by specifying a state update function and an output function. The new
system can be a continuous or discrete time system. Nonlinear I/O
systems are usually created with the :func:`~control.nlsys` factory
function.

Parameters
----------
Expand All @@ -63,20 +64,13 @@ class NonlinearIOSystem(InputOutputSystem):

where the arguments are the same as for `upfcn`.

inputs : int, list of str or None, optional
Description of the system inputs. This can be given as an integer
count or as a list of strings that name the individual signals.
If an integer count is specified, the names of the signal will be
of the form 's[i]' (where 's' is one of 'u', 'y', or 'x'). If
this parameter is not given or given as `None`, the relevant
quantity will be determined when possible based on other
information provided to functions using the system.

outputs : int, list of str or None, optional
Description of the system outputs. Same format as `inputs`.
inputs, outputs, states : int, list of str or None, optional
Description of the system inputs, outputs, and states. See
:func:`control.nlsys` for more details.

states : int, list of str, or None, optional
Description of the system states. Same format as `inputs`.
params : dict, optional
Parameter values for the systems. Passed to the evaluation functions
for the system as default values, overriding internal defaults.

dt : timebase, optional
The timebase for the system, used to specify whether the system is
Expand All @@ -88,13 +82,16 @@ class NonlinearIOSystem(InputOutputSystem):
* dt = True: discrete time with unspecified sampling period
* dt = None: no timebase specified

Attributes
----------
ninputs, noutputs, nstates : int
Number of input, output and state variables.
shape : tuple
2-tuple of I/O system dimension, (noutputs, ninputs).
input_labels, output_labels, state_labels : list of str
Names for the input, output, and state variables.
name : string, optional
System name (used for specifying signals). If unspecified, a
generic name <sys[id]> is generated with a unique integer id.

params : dict, optional
Parameter values for the system. Passed to the evaluation functions
for the system as default values, overriding internal defaults.
System name.

See Also
--------
Expand Down Expand Up @@ -1220,8 +1217,7 @@ def check_unused_signals(
return dropped_inputs, dropped_outputs


def nlsys(
updfcn, outfcn=None, inputs=None, outputs=None, states=None, **kwargs):
def nlsys(updfcn, outfcn=None, **kwargs):
"""Create a nonlinear input/output system.

Creates an :class:`~control.InputOutputSystem` for a nonlinear system by
Expand All @@ -1230,7 +1226,7 @@ def nlsys(

Parameters
----------
updfcn : callable
updfcn : callable (or StateSpace)
Function returning the state update function

`updfcn(t, x, u, params) -> array`
Expand All @@ -1240,6 +1236,10 @@ def nlsys(
time, and `params` is a dict containing the values of parameters
used by the function.

If a :class:`StateSpace` system is passed as the update function,
then a nonlinear I/O system is created that implements the linear
dynamics of the state space system.

outfcn : callable
Function returning the output at the given state

Expand Down Expand Up @@ -1308,9 +1308,31 @@ def nlsys(
>>> timepts = np.linspace(0, 10)
>>> response = ct.input_output_response(
... kincar, timepts, [10, 0.05 * np.sin(timepts)])

"""
return NonlinearIOSystem(
updfcn, outfcn, inputs=inputs, outputs=outputs, states=states, **kwargs)
from .statesp import StateSpace
from .iosys import _extended_system_name

if isinstance(updfcn, StateSpace):
sys_ss = updfcn
kwargs['inputs'] = kwargs.get('inputs', sys_ss.input_labels)
kwargs['outputs'] = kwargs.get('outputs', sys_ss.output_labels)
kwargs['states'] = kwargs.get('states', sys_ss.state_labels)
kwargs['name'] = kwargs.get('name', _extended_system_name(
sys_ss.name, prefix_suffix_name='converted'))

sys_nl = NonlinearIOSystem(
lambda t, x, u, params: sys.A @ x + sys.B @ u,
lambda t, x, u, params: sys.C @ x + sys.D @ u, **kwargs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
lambda t, x, u, params: sys.A @ x + sys.B @ u,
lambda t, x, u, params: sys.C @ x + sys.D @ u, **kwargs)
lambda t, x, u, params: sys_ss.A @ x + sys_ss.B @ u,
lambda t, x, u, params: sys_ss.C @ x + sys_ss.D @ u, **kwargs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug and needs to be fixed before merging.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on this. In the latest update, I added a unit test that catches the bug (and fixed the bug).


if sys_nl.nstates != sys_ss.nstates or sys_nl.shape != sys_ss.shape:
raise ValueError(
"new input, output, or state specification "
"doesn't match system size")

return sys_nl
else:
return NonlinearIOSystem(updfcn, outfcn, **kwargs)


def input_output_response(
Expand Down
14 changes: 9 additions & 5 deletions control/tests/docstrings_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def test_deprecated_functions(module, prefix):
ct.FrequencyResponseData: ct.frd,
ct.InterconnectedSystem: ct.interconnect,
ct.LinearICSystem: ct.interconnect,
# (ct.NonlinearIOSystem: ct.nlsys,
ct.NonlinearIOSystem: ct.nlsys,
ct.StateSpace: ct.ss,
ct.TransferFunction: ct.tf,
}
Expand All @@ -285,20 +285,21 @@ def test_deprecated_functions(module, prefix):
class_args = {
# fs.FlatSystem: ['forward', 'reverse', 'updfcn', 'outfcn'],
ct.FrequencyResponseData: ['response', 'omega', 'dt'],
# (ct.NonlinearIOSystem: ['updfcn', 'outfcn', 'dt'],
ct.NonlinearIOSystem: [
'updfcn', 'outfcn', 'inputs', 'outputs', 'states', 'params', 'dt'],
ct.StateSpace: ['A', 'B', 'C', 'D', 'dt'],
ct.TransferFunction: ['num', 'den', 'dt'],
}

# List of attributes defined for all I/O systems
std_class_attributes = [
'ninputs', 'noutputs', 'input_labels', 'output_labels', 'name']
'ninputs', 'noutputs', 'input_labels', 'output_labels', 'name', 'shape']

# List of attributes defined for specific I/O systems
class_attributes = {
# fs.FlatSystem: [],
ct.FrequencyResponseData: [],
# (ct.NonlinearIOSystem: [],
ct.NonlinearIOSystem: [],
ct.StateSpace: ['nstates', 'state_labels'],
ct.TransferFunction: [],
}
Expand All @@ -316,7 +317,7 @@ def test_deprecated_functions(module, prefix):
factory_args = {
# fs.flatsys: [],
ct.frd: ['sys'],
# fs.nlsys: [],
ct.nlsys: [],
ct.ss: ['sys', 'states'],
ct.tf: ['sys'],
}
Expand Down Expand Up @@ -363,6 +364,9 @@ def test_iosys_attribute_lists(cls, ignore_future_warning):
case ct.frd:
sys = ct.frd(sys, [0.1, 1, 10])
ignore_args = ['state_labels']
case ct.nlsys:
sys = ct.nlsys(sys)
ignore_args = []
case _:
ignore_args = []

Expand Down
46 changes: 44 additions & 2 deletions control/tests/nlsys_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@

"""

import pytest
import numpy as np
import math
import re

import numpy as np
import pytest

import control as ct


# Basic test of nlsys()
def test_nlsys_basic():
def kincar_update(t, x, u, params):
Expand Down Expand Up @@ -154,3 +158,41 @@ def test_nlsys_empty_io():

resp = ct.forced_response(P, np.linspace(0, 1), 1)
np.testing.assert_allclose(resp.states[:, -1], 1 - math.exp(-1))


def test_ss2io():
sys = ct.rss(
states=4, inputs=['u1', 'u2'], outputs=['y1', 'y2'], name='sys')

# Standard conversion
nlsys = ct.nlsys(sys)
for attr in ['nstates', 'ninputs', 'noutputs']:
assert getattr(nlsys, attr) == getattr(sys, attr)
assert nlsys.name == 'sys$converted'

# Put names back to defaults
nlsys = ct.nlsys(
sys, inputs=sys.ninputs, outputs=sys.noutputs, states=sys.nstates)
for attr, prefix in zip(
['state_labels', 'input_labels', 'output_labels'],
['x', 'u', 'y']):
for i in range(len(getattr(nlsys, attr))):
assert getattr(nlsys, attr)[i] == f"{prefix}[{i}]"
assert re.match(r"sys\$converted", nlsys.name)

# Override the names with something new
nlsys = ct.nlsys(
sys, inputs=['U1', 'U2'], outputs=['Y1', 'Y2'],
states=['X1', 'X2', 'X3', 'X4'], name='nlsys')
for attr, prefix in zip(
['state_labels', 'input_labels', 'output_labels'],
['X', 'U', 'Y']):
for i in range(len(getattr(nlsys, attr))):
assert getattr(nlsys, attr)[i] == f"{prefix}{i+1}"
assert nlsys.name == 'nlsys'

# Make sure dimension checking works
for attr in ['states', 'inputs', 'outputs']:
with pytest.raises(ValueError, match=r"new .* doesn't match"):
kwargs = {attr: getattr(sys, 'n' + attr) - 1}
nlsys = ct.nlsys(sys, **kwargs)