Skip to content

Commit 7dc92df

Browse files
committed
add nlsys() function for creating nonlinear I/O systems
1 parent 0758426 commit 7dc92df

8 files changed

Lines changed: 165 additions & 29 deletions

File tree

control/nlsys.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from .timeresp import _check_convert_array, _process_time_response, \
3030
TimeResponseData
3131

32-
__all__ = ['NonlinearIOSystem', 'InterconnectedSystem',
32+
__all__ = ['NonlinearIOSystem', 'InterconnectedSystem', 'nlsys',
3333
'input_output_response', 'find_eqpt', 'linearize',
3434
'interconnect']
3535

@@ -1133,6 +1133,100 @@ def check_unused_signals(
11331133
return dropped_inputs, dropped_outputs
11341134

11351135

1136+
def nlsys(
1137+
updfcn, outfcn=None, inputs=None, outputs=None, states=None, **kwargs):
1138+
"""Create a nonlinear input/output system.
1139+
1140+
Creates an :class:`~control.InputOutputSystem` for a nonlinear system by
1141+
specifying a state update function and an output function. The new system
1142+
can be a continuous or discrete time system.
1143+
1144+
Parameters
1145+
----------
1146+
updfcn : callable
1147+
Function returning the state update function
1148+
1149+
`updfcn(t, x, u, params) -> array`
1150+
1151+
where `x` is a 1-D array with shape (nstates,), `u` is a 1-D array
1152+
with shape (ninputs,), `t` is a float representing the currrent
1153+
time, and `params` is a dict containing the values of parameters
1154+
used by the function.
1155+
1156+
outfcn : callable
1157+
Function returning the output at the given state
1158+
1159+
`outfcn(t, x, u, params) -> array`
1160+
1161+
where the arguments are the same as for `upfcn`.
1162+
1163+
inputs : int, list of str or None, optional
1164+
Description of the system inputs. This can be given as an integer
1165+
count or as a list of strings that name the individual signals.
1166+
If an integer count is specified, the names of the signal will be
1167+
of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If
1168+
this parameter is not given or given as `None`, the relevant
1169+
quantity will be determined when possible based on other
1170+
information provided to functions using the system.
1171+
1172+
outputs : int, list of str or None, optional
1173+
Description of the system outputs. Same format as `inputs`.
1174+
1175+
states : int, list of str, or None, optional
1176+
Description of the system states. Same format as `inputs`.
1177+
1178+
dt : timebase, optional
1179+
The timebase for the system, used to specify whether the system is
1180+
operating in continuous or discrete time. It can have the
1181+
following values:
1182+
1183+
* dt = 0: continuous time system (default)
1184+
* dt > 0: discrete time system with sampling period 'dt'
1185+
* dt = True: discrete time with unspecified sampling period
1186+
* dt = None: no timebase specified
1187+
1188+
name : string, optional
1189+
System name (used for specifying signals). If unspecified, a
1190+
generic name <sys[id]> is generated with a unique integer id.
1191+
1192+
params : dict, optional
1193+
Parameter values for the systems. Passed to the evaluation
1194+
functions for the system as default values, overriding internal
1195+
defaults.
1196+
1197+
Returns
1198+
-------
1199+
sys : :class:`NonlinearIOSystem`
1200+
Nonlinear input/output system.
1201+
1202+
See Also
1203+
--------
1204+
ss, tf
1205+
1206+
Example
1207+
-------
1208+
>>> def kincar_update(t, x, u, params):
1209+
... l = params.get('l', 1) # wheelbase
1210+
... return np.array([
1211+
... np.cos(x[2]) * u[0], # x velocity
1212+
... np.sin(x[2]) * u[0], # y velocity
1213+
... np.tan(u[1]) * u[0] / l # angular velocity
1214+
... ])
1215+
>>>
1216+
>>> def kincar_output(t, x, u, params):
1217+
... return x[0:2] # x, y position
1218+
>>>
1219+
>>> kincar = ct.nlsys(
1220+
... kincar_update, kincar_output, states=3, inputs=2, outputs=2)
1221+
>>>
1222+
>>> timepts = np.linspace(0, 10)
1223+
>>> response = ct.input_output_response(
1224+
... kincar, timepts, [10, 0.05 * np.sin(timepts)])
1225+
"""
1226+
return NonlinearIOSystem(
1227+
updfcn, outfcn, inputs=inputs, outputs=outputs, states=states, **kwargs)
1228+
1229+
11361230
def input_output_response(
11371231
sys, T, U=0., X0=0, params=None,
11381232
transpose=False, return_x=False, squeeze=None,

control/tests/config_test.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,11 @@ def test_legacy_defaults(self):
254254
assert isinstance(ct.ss(0, 0, 0, 1).D, np.ndarray)
255255
assert not isinstance(ct.ss(0, 0, 0, 1).D, np.matrix)
256256

257-
# test that old versions don't raise a problem
258-
ct.use_legacy_defaults('REL-0.1')
259-
ct.use_legacy_defaults('control-0.3a')
260-
ct.use_legacy_defaults('0.6c')
261-
ct.use_legacy_defaults('0.8.2')
262-
ct.use_legacy_defaults('0.1')
257+
# test that old versions don't raise a problem (besides Numpy warning)
258+
for ver in ['REL-0.1', 'control-0.3a', '0.6c', '0.8.2', '0.1']:
259+
with pytest.warns(
260+
UserWarning, match="NumPy matrix class no longer"):
261+
ct.use_legacy_defaults(ver)
263262

264263
# Make sure that nonsense versions generate an error
265264
with pytest.raises(ValueError):
@@ -273,7 +272,7 @@ def test_change_default_dt(self, dt):
273272
ct.set_defaults('control', default_dt=dt)
274273
assert ct.ss(1, 0, 0, 1).dt == dt
275274
assert ct.tf(1, [1, 1]).dt == dt
276-
nlsys = ct.nlsys.NonlinearIOSystem(
275+
nlsys = ct.NonlinearIOSystem(
277276
lambda t, x, u: u * x * x,
278277
lambda t, x, u: x, inputs=1, outputs=1)
279278
assert nlsys.dt == dt

control/tests/iosys_test.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,8 +1897,9 @@ def test_nonuniform_timepts(nstates, noutputs, ninputs):
18971897

18981898
def test_ss_nonlinear():
18991899
"""Test ss() for creating nonlinear systems"""
1900-
secord = ct.ss(secord_update, secord_output, inputs='u', outputs='y',
1901-
states = ['x1', 'x2'], name='secord')
1900+
with pytest.warns(PendingDeprecationWarning, match="use nlsys()"):
1901+
secord = ct.ss(secord_update, secord_output, inputs='u', outputs='y',
1902+
states = ['x1', 'x2'], name='secord')
19021903
assert secord.name == 'secord'
19031904
assert secord.input_labels == ['u']
19041905
assert secord.output_labels == ['y']
@@ -1917,12 +1918,14 @@ def test_ss_nonlinear():
19171918
np.testing.assert_almost_equal(ss_response.outputs, io_response.outputs)
19181919

19191920
# Make sure that optional keywords are allowed
1920-
secord = ct.ss(secord_update, secord_output, dt=True)
1921+
with pytest.warns(PendingDeprecationWarning, match="use nlsys()"):
1922+
secord = ct.ss(secord_update, secord_output, dt=True)
19211923
assert ct.isdtime(secord)
19221924

19231925
# Make sure that state space keywords are flagged
1924-
with pytest.raises(TypeError, match="unrecognized keyword"):
1925-
ct.ss(secord_update, remove_useless_states=True)
1926+
with pytest.warns(PendingDeprecationWarning, match="use nlsys()"):
1927+
with pytest.raises(TypeError, match="unrecognized keyword"):
1928+
ct.ss(secord_update, remove_useless_states=True)
19261929

19271930

19281931
def test_rss():

control/tests/kwargs_test.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ def test_kwarg_search(module, prefix):
7070
source = inspect.getsource(kwarg_unittest[prefix + name])
7171

7272
# Make sure the unit test looks for unrecognized keyword
73-
if source and source.find('unrecognized keyword') < 0:
73+
if kwarg_unittest[prefix + name] == test_unrecognized_kwargs:
74+
# @parametrize messes up the check, but we know it is there
75+
pass
76+
77+
elif source and source.find('unrecognized keyword') < 0:
7478
warnings.warn(
7579
f"'unrecognized keyword' not found in unit test "
7680
f"for {name}")
@@ -85,6 +89,7 @@ def test_kwarg_search(module, prefix):
8589
(control.lqe, 1, 0, ([[1]], [[1]]), {}),
8690
(control.lqr, 1, 0, ([[1, 0], [0, 1]], [[1]]), {}),
8791
(control.linearize, 1, 0, (0, 0), {}),
92+
(control.nlsys, 0, 0, (lambda t, x, u, params: np.array([0]),), {}),
8893
(control.pzmap, 1, 0, (), {}),
8994
(control.rlocus, 0, 1, (), {}),
9095
(control.root_locus, 0, 1, (), {}),
@@ -172,6 +177,7 @@ def test_matplotlib_kwargs(function, nsysargs, moreargs, kwargs, mplcleanup):
172177
'linearize': test_unrecognized_kwargs,
173178
'lqe': test_unrecognized_kwargs,
174179
'lqr': test_unrecognized_kwargs,
180+
'nlsys': test_unrecognized_kwargs,
175181
'nyquist': test_matplotlib_kwargs,
176182
'nyquist_plot': test_matplotlib_kwargs,
177183
'pzmap': test_unrecognized_kwargs,

control/tests/nlsys_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""nlsys_test.py - test nonlinear input/output system operations
2+
3+
RMM, 18 Jun 2022
4+
5+
This test suite checks various newer functions for NonlinearIOSystems.
6+
The main test functions are contained in iosys_test.py.
7+
8+
"""
9+
10+
import pytest
11+
import numpy as np
12+
import control as ct
13+
14+
def test_nlsys_basic():
15+
def kincar_update(t, x, u, params):
16+
l = params.get('l', 1) # wheelbase
17+
return np.array([
18+
np.cos(x[2]) * u[0], # x velocity
19+
np.sin(x[2]) * u[0], # y velocity
20+
np.tan(u[1]) * u[0] / l # angular velocity
21+
])
22+
23+
def kincar_output(t, x, u, params):
24+
return x[0:2] # x, y position
25+
26+
kincar = ct.nlsys(
27+
kincar_update, kincar_output,
28+
states=['x', 'y', 'theta'],
29+
inputs=2, input_prefix='U',
30+
outputs=2)
31+
assert kincar.input_labels == ['U[0]', 'U[1]']
32+
assert kincar.output_labels == ['y[0]', 'y[1]']
33+
assert kincar.state_labels == ['x', 'y', 'theta']

control/tests/timeresp_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,9 @@ def test_forced_response_legacy(self):
639639
U = np.sin(T)
640640

641641
"""Make sure that legacy version of forced_response works"""
642-
ct.config.use_legacy_defaults("0.8.4")
642+
with pytest.warns(
643+
UserWarning, match="NumPy matrix class no longer"):
644+
ct.config.use_legacy_defaults("0.8.4")
643645
# forced_response returns x by default
644646
t, y = ct.step_response(sys, T)
645647
t, y, x = ct.forced_response(sys, T, U)

doc/control.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ System creation
2121
zpk
2222
rss
2323
drss
24-
NonlinearIOSystem
24+
nlsys
2525

2626

2727
System interconnections
@@ -193,7 +193,6 @@ Utility functions and conversions
193193
tf2ss
194194
tfdata
195195
timebase
196-
timebaseEqual
197196
unwrap
198197
use_fbs_defaults
199198
use_matlab_defaults

doc/iosys.rst

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ a :class:`~control.StateSpace` linear system. Use the
2626

2727
Input/output systems are automatically created for state space LTI systems
2828
when using the :func:`ss` function. Nonlinear input/output systems can be
29-
created using the :class:`~control.NonlinearIOSystem` class, which requires
29+
created using the :func:`~control.nlsys` function, which requires
3030
the definition of an update function (for the right hand side of the
3131
differential or different equation) and an output function (computes the
3232
outputs from the state)::
3333

34-
io_sys = NonlinearIOSystem(updfcn, outfcn, inputs=M, outputs=P, states=N)
34+
io_sys = ct.nlsys(updfcn, outfcn, inputs=M, outputs=P, states=N)
3535

3636
More complex input/output systems can be constructed by using the
3737
:func:`~control.interconnect` function, which allows a collection of
@@ -92,7 +92,7 @@ We now create an input/output system using these dynamics:
9292

9393
.. code-block:: python
9494
95-
io_predprey = ct.NonlinearIOSystem(
95+
io_predprey = ct.nlsys(
9696
predprey_rhs, None, inputs=('u'), outputs=('H', 'L'),
9797
states=('H', 'L'), name='predprey')
9898
@@ -141,21 +141,20 @@ lynxes as the desired output (following FBS2e, Example 7.5):
141141
To construct the control law, we build a simple input/output system that
142142
applies a corrective input based on deviations from the equilibrium point.
143143
This system has no dynamics, since it is a static (affine) map, and can
144-
constructed using the `~control.ios.NonlinearIOSystem` class:
144+
constructed using :func:`~control.nlsys` with no update function:
145145

146146
.. code-block:: python
147147
148-
io_controller = ct.NonlinearIOSystem(
148+
io_controller = ct.nlsys(
149149
None,
150150
lambda t, x, u, params: -K @ (u[1:] - xeq) + kf * (u[0] - xeq[1]),
151151
inputs=('Ld', 'u1', 'u2'), outputs=1, name='control')
152152
153153
The input to the controller is `u`, consisting of the vector of hare and lynx
154154
populations followed by the desired lynx population.
155155

156-
To connect the controller to the predatory-prey model, we create an
157-
:class:`~control.InterconnectedSystem` using the :func:`~control.interconnect`
158-
function:
156+
To connect the controller to the predatory-prey model, we use the
157+
:func:`~control.interconnect` function:
159158

160159
.. code-block:: python
161160
@@ -243,8 +242,8 @@ interconnecting systems, especially when combined with the
243242
:func:`~control.summing_junction` function. For example, the following code
244243
will create a unity gain, negative feedback system::
245244

246-
P = ct.tf2io([1], [1, 0], inputs='u', outputs='y')
247-
C = ct.tf2io([10], [1, 1], inputs='e', outputs='u')
245+
P = ct.tf([1], [1, 0], inputs='u', outputs='y')
246+
C = ct.tf([10], [1, 1], inputs='e', outputs='u')
248247
sumblk = ct.summing_junction(inputs=['r', '-y'], output='e')
249248
T = ct.interconnect([P, C, sumblk], inplist='r', outlist='y')
250249

@@ -471,7 +470,8 @@ Module classes and functions
471470
:toctree: generated/
472471

473472
~control.find_eqpt
474-
~control.linearize
475-
~control.input_output_response
476473
~control.interconnect
474+
~control.input_output_response
475+
~control.linearize
476+
~control.nlsys
477477
~control.summing_junction

0 commit comments

Comments
 (0)