Skip to content

Commit ad714fe

Browse files
committed
allow creation of NonlinearIOSystem via ss()
1 parent 96d813c commit ad714fe

File tree

4 files changed

+43
-7
lines changed

4 files changed

+43
-7
lines changed

control/iosys.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import copy
3232
from warnings import warn
3333

34+
from .lti import LTI
3435
from .namedio import _NamedIOSystem, _process_signal_list
3536
from .statesp import StateSpace, tf2ss, _convert_to_statespace
3637
from .statesp import _ss, _rss_generate
@@ -2252,12 +2253,17 @@ def ss(*args, **kwargs):
22522253
22532254
Create a state space system.
22542255
2255-
The function accepts either 1, 4 or 5 parameters:
2256+
The function accepts either 1, 2, 4 or 5 parameters:
22562257
22572258
``ss(sys)``
22582259
Convert a linear system into space system form. Always creates a
22592260
new system, even if sys is already a state space system.
22602261
2262+
``ss(updfcn, outfucn)```
2263+
Create a nonlinear input/output system with update function ``updfcn``
2264+
and output function ``outfcn``. See :class:`NonlinearIOSystem` for
2265+
more information.
2266+
22612267
``ss(A, B, C, D)``
22622268
Create a state space system from the matrices of its state and
22632269
output equations:
@@ -2280,6 +2286,10 @@ def ss(*args, **kwargs):
22802286
Everything that the constructor of :class:`numpy.matrix` accepts is
22812287
permissible here too.
22822288
2289+
``ss(args, inputs=['u1', ..., 'up'], outputs=['y1', ..., 'yq'],
2290+
states=['x1', ..., 'xn'])
2291+
Create a system with named input, output, and state signals.
2292+
22832293
Parameters
22842294
----------
22852295
sys : StateSpace or TransferFunction
@@ -2326,6 +2336,12 @@ def ss(*args, **kwargs):
23262336
>>> sys2 = ss(sys_tf)
23272337
23282338
"""
2339+
# See if this is a nonlinear I/O system
2340+
if len(args) > 0 and hasattr(args[0], '__call__') and \
2341+
not isinstance (args[0], (InputOutputSystem, LTI)):
2342+
# Function as first argument => assume nonlinear IO system
2343+
return NonlinearIOSystem(*args, **kwargs)
2344+
23292345
# Extract the keyword arguments needed for StateSpace (via _ss)
23302346
ss_kwlist = ('dt', 'remove_useless_states')
23312347
ss_kwargs = {}
@@ -2334,7 +2350,7 @@ def ss(*args, **kwargs):
23342350
ss_kwargs[kw] = kwargs.pop(kw)
23352351

23362352
# Create the statespace system and then convert to I/O system
2337-
sys = _ss(*args, keywords=ss_kwargs)
2353+
sys = _ss(*args, **ss_kwargs)
23382354
return LinearIOSystem(sys, **kwargs)
23392355

23402356

control/tests/iosys_test.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ def test_iosys_print(self, tsys, capsys):
130130
print(ios_linearized)
131131

132132
@noscipy0
133-
def test_nonlinear_iosys(self, tsys):
133+
@pytest.mark.parametrize("ss", [ios.NonlinearIOSystem, ct.ss])
134+
def test_nonlinear_iosys(self, tsys, ss):
134135
# Create a simple nonlinear I/O system
135-
nlsys = ios.NonlinearIOSystem(predprey)
136+
nlsys = ss(predprey)
136137
T = tsys.T
137138

138139
# Start by simulating from an equilibrium point
@@ -159,7 +160,7 @@ def test_nonlinear_iosys(self, tsys):
159160
np.reshape(linsys.C @ np.reshape(x, (-1, 1))
160161
+ linsys.D @ np.reshape(u, (-1, 1)),
161162
(-1,))
162-
nlsys = ios.NonlinearIOSystem(nlupd, nlout, inputs=1, outputs=1)
163+
nlsys = ss(nlupd, nlout, inputs=1, outputs=1)
163164

164165
# Make sure that simulations also line up
165166
T, U, X0 = tsys.T, tsys.U, tsys.X0
@@ -1775,3 +1776,21 @@ def test_nonuniform_timepts():
17751776
t_even, y_even = ct.input_output_response(
17761777
sys, noufpts, nonunif, t_eval=unifpts)
17771778
np.testing.assert_almost_equal(y_unif, y_even, decimal=6)
1779+
1780+
1781+
def test_ss_nonlinear():
1782+
"""Test ss() for creating nonlinear systems"""
1783+
secord = ct.ss(secord_update, secord_output, inputs='u', outputs='y',
1784+
states = ['x1', 'x2'], name='secord')
1785+
assert secord.name == 'secord'
1786+
assert secord.input_labels == ['u']
1787+
assert secord.output_labels == ['y']
1788+
assert secord.state_labels == ['x1', 'x2']
1789+
1790+
# Make sure that optional keywords are allowed
1791+
secord = ct.ss(secord_update, secord_output, dt=True)
1792+
assert ct.isdtime(secord)
1793+
1794+
# Make sure that state space keywords are flagged
1795+
with pytest.raises(TypeError, match="unrecognized keyword"):
1796+
ct.ss(secord_update, remove_useless_states=True)

control/tests/timeresp_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ def test_time_vector(self, tsystem, fun, squeeze, matarrayout):
875875
kw['U'] = np.vstack([np.sin(t) for i in range(sys.ninputs)])
876876
elif fun == forced_response and isctime(sys, strict=True):
877877
pytest.skip("No continuous forced_response without time vector.")
878-
if hasattr(sys, "nstates"):
878+
if hasattr(sys, "nstates") and sys.nstates is not None:
879879
kw['X0'] = np.arange(sys.nstates) + 1
880880
if sys.ninputs > 1 and fun in [step_response, impulse_response]:
881881
kw['input'] = 1

control/xferfcn.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from re import sub
6262
from .lti import LTI, common_timebase, isdtime, _process_frequency_response
6363
from .exception import ControlMIMONotImplemented
64+
from .namedio import _NamedIOSystem, _process_signal_list
6465
from . import config
6566

6667
__all__ = ['TransferFunction', 'tf', 'ss2tf', 'tfdata']
@@ -70,7 +71,7 @@
7071
_xferfcn_defaults = {}
7172

7273

73-
class TransferFunction(LTI):
74+
class TransferFunction(LTI, _NamedIOSystem):
7475
"""TransferFunction(num, den[, dt])
7576
7677
A class for representing transfer functions.

0 commit comments

Comments
 (0)