Skip to content

Commit c82605b

Browse files
Merge branch 'main' into fix_isstatic-12Nov2022
2 parents c425d2c + b32e355 commit c82605b

File tree

4 files changed

+126
-46
lines changed

4 files changed

+126
-46
lines changed

control/iosys.py

+57-40
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class for a set of subclasses that are used to implement specific
126126
# Allow ndarray * InputOutputSystem to give IOSystem._rmul_() priority
127127
__array_priority__ = 12 # override ndarray, matrix, SS types
128128

129-
def __init__(self, params={}, **kwargs):
129+
def __init__(self, params=None, **kwargs):
130130
"""Create an input/output system.
131131
132132
The InputOutputSystem constructor is used to create an input/output
@@ -148,7 +148,7 @@ def __init__(self, params={}, **kwargs):
148148
states=states, name=name, dt=dt)
149149

150150
# default parameters
151-
self.params = params.copy()
151+
self.params = {} if params is None else params.copy()
152152

153153
def __mul__(sys2, sys1):
154154
"""Multiply two input/output systems (series interconnection)"""
@@ -357,7 +357,7 @@ def _update_params(self, params, warning=False):
357357
if warning:
358358
warn("Parameters passed to InputOutputSystem ignored.")
359359

360-
def _rhs(self, t, x, u, params={}):
360+
def _rhs(self, t, x, u):
361361
"""Evaluate right hand side of a differential or difference equation.
362362
363363
Private function used to compute the right hand side of an
@@ -369,23 +369,24 @@ def _rhs(self, t, x, u, params={}):
369369
NotImplemented("Evaluation not implemented for system of type ",
370370
type(self))
371371

372-
def dynamics(self, t, x, u):
372+
def dynamics(self, t, x, u, params=None):
373373
"""Compute the dynamics of a differential or difference equation.
374374
375375
Given time `t`, input `u` and state `x`, returns the value of the
376376
right hand side of the dynamical system. If the system is continuous,
377377
returns the time derivative
378378
379-
dx/dt = f(t, x, u)
379+
dx/dt = f(t, x, u[, params])
380380
381381
where `f` is the system's (possibly nonlinear) dynamics function.
382382
If the system is discrete-time, returns the next value of `x`:
383383
384-
x[t+dt] = f(t, x[t], u[t])
384+
x[t+dt] = f(t, x[t], u[t][, params])
385385
386-
Where `t` is a scalar.
386+
where `t` is a scalar.
387387
388-
The inputs `x` and `u` must be of the correct length.
388+
The inputs `x` and `u` must be of the correct length. The `params`
389+
argument is an optional dictionary of parameter values.
389390
390391
Parameters
391392
----------
@@ -395,14 +396,17 @@ def dynamics(self, t, x, u):
395396
current state
396397
u : array_like
397398
input
399+
params : dict (optional)
400+
system parameter values
398401
399402
Returns
400403
-------
401404
dx/dt or x[t+dt] : ndarray
402405
"""
406+
self._update_params(params)
403407
return self._rhs(t, x, u)
404408

405-
def _out(self, t, x, u, params={}):
409+
def _out(self, t, x, u):
406410
"""Evaluate the output of a system at a given state, input, and time
407411
408412
Private function used to compute the output of of an input/output
@@ -414,13 +418,13 @@ def _out(self, t, x, u, params={}):
414418
# If no output function was defined in subclass, return state
415419
return x
416420

417-
def output(self, t, x, u):
421+
def output(self, t, x, u, params=None):
418422
"""Compute the output of the system
419423
420424
Given time `t`, input `u` and state `x`, returns the output of the
421425
system:
422426
423-
y = g(t, x, u)
427+
y = g(t, x, u[, params])
424428
425429
The inputs `x` and `u` must be of the correct length.
426430
@@ -432,14 +436,17 @@ def output(self, t, x, u):
432436
current state
433437
u : array_like
434438
input
439+
params : dict (optional)
440+
system parameter values
435441
436442
Returns
437443
-------
438444
y : ndarray
439445
"""
446+
self._update_params(params)
440447
return self._out(t, x, u)
441448

442-
def feedback(self, other=1, sign=-1, params={}):
449+
def feedback(self, other=1, sign=-1, params=None):
443450
"""Feedback interconnection between two input/output systems
444451
445452
Parameters
@@ -507,7 +514,7 @@ def feedback(self, other=1, sign=-1, params={}):
507514
# Return the newly created system
508515
return newsys
509516

510-
def linearize(self, x0, u0, t=0, params={}, eps=1e-6,
517+
def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
511518
name=None, copy=False, **kwargs):
512519
"""Linearize an input/output system at a given state and input.
513520
@@ -651,7 +658,7 @@ def __init__(self, linsys, **kwargs):
651658
# Note: don't use super() to override StateSpace MRO
652659
InputOutputSystem.__init__(
653660
self, inputs=inputs, outputs=outputs, states=states,
654-
params={}, dt=dt, name=name)
661+
params=None, dt=dt, name=name)
655662

656663
# Initalize additional state space variables
657664
StateSpace.__init__(
@@ -668,7 +675,7 @@ def __init__(self, linsys, **kwargs):
668675
#: number of states, use :attr:`nstates`.
669676
states = property(StateSpace._get_states, StateSpace._set_states)
670677

671-
def _update_params(self, params={}, warning=True):
678+
def _update_params(self, params=None, warning=True):
672679
# Parameters not supported; issue a warning
673680
if params and warning:
674681
warn("Parameters passed to LinearIOSystems are ignored.")
@@ -756,7 +763,7 @@ class NonlinearIOSystem(InputOutputSystem):
756763
defaults.
757764
758765
"""
759-
def __init__(self, updfcn, outfcn=None, params={}, **kwargs):
766+
def __init__(self, updfcn, outfcn=None, params=None, **kwargs):
760767
"""Create a nonlinear I/O system given update and output functions."""
761768
# Process keyword arguments
762769
name, inputs, outputs, states, dt = _process_namedio_keywords(
@@ -791,7 +798,7 @@ def __init__(self, updfcn, outfcn=None, params={}, **kwargs):
791798
"(and nstates not known).")
792799

793800
# Initialize current parameters to default parameters
794-
self._current_params = params.copy()
801+
self._current_params = {} if params is None else params.copy()
795802

796803
def __str__(self):
797804
return f"{InputOutputSystem.__str__(self)}\n\n" + \
@@ -838,7 +845,8 @@ def __call__(sys, u, params=None, squeeze=None):
838845
def _update_params(self, params, warning=False):
839846
# Update the current parameter values
840847
self._current_params = self.params.copy()
841-
self._current_params.update(params)
848+
if params:
849+
self._current_params.update(params)
842850

843851
def _rhs(self, t, x, u):
844852
xdot = self.updfcn(t, x, u, self._current_params) \
@@ -862,20 +870,22 @@ class InterconnectedSystem(InputOutputSystem):
862870
See :func:`~control.interconnect` for a list of parameters.
863871
864872
"""
865-
def __init__(self, syslist, connections=[], inplist=[], outlist=[],
866-
params={}, warn_duplicate=None, **kwargs):
873+
def __init__(self, syslist, connections=None, inplist=None, outlist=None,
874+
params=None, warn_duplicate=None, **kwargs):
867875
"""Create an I/O system from a list of systems + connection info."""
868876
# Convert input and output names to lists if they aren't already
869-
if not isinstance(inplist, (list, tuple)):
877+
if inplist is not None and not isinstance(inplist, (list, tuple)):
870878
inplist = [inplist]
871-
if not isinstance(outlist, (list, tuple)):
879+
if outlist is not None and not isinstance(outlist, (list, tuple)):
872880
outlist = [outlist]
873881

874882
# Check if dt argument was given; if not, pull from systems
875883
dt = kwargs.pop('dt', None)
876884

877885
# Process keyword arguments (except dt)
878-
defaults = {'inputs': len(inplist), 'outputs': len(outlist)}
886+
defaults = {
887+
'inputs': len(inplist or []),
888+
'outputs': len(outlist or [])}
879889
name, inputs, outputs, states, _ = _process_namedio_keywords(
880890
kwargs, defaults, end=True)
881891

@@ -894,6 +904,12 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
894904

895905
# Go through the system list and keep track of counts, offsets
896906
for sysidx, sys in enumerate(syslist):
907+
# If we were passed a SS or TF system, convert to LinearIOSystem
908+
if isinstance(sys, (StateSpace, TransferFunction)) and \
909+
not isinstance(sys, LinearIOSystem):
910+
sys = LinearIOSystem(sys)
911+
syslist[sysidx] = sys
912+
897913
# Make sure time bases are consistent
898914
dt = common_timebase(dt, sys.dt)
899915

@@ -969,7 +985,7 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
969985

970986
# Convert the list of interconnections to a connection map (matrix)
971987
self.connect_map = np.zeros((ninputs, noutputs))
972-
for connection in connections:
988+
for connection in connections or []:
973989
input_index = self._parse_input_spec(connection[0])
974990
for output_spec in connection[1:]:
975991
output_index, gain = self._parse_output_spec(output_spec)
@@ -980,7 +996,7 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
980996

981997
# Convert the input list to a matrix: maps system to subsystems
982998
self.input_map = np.zeros((ninputs, self.ninputs))
983-
for index, inpspec in enumerate(inplist):
999+
for index, inpspec in enumerate(inplist or []):
9841000
if isinstance(inpspec, (int, str, tuple)):
9851001
inpspec = [inpspec]
9861002
if not isinstance(inpspec, list):
@@ -995,7 +1011,7 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
9951011

9961012
# Convert the output list to a matrix: maps subsystems to system
9971013
self.output_map = np.zeros((self.noutputs, noutputs + ninputs))
998-
for index, outspec in enumerate(outlist):
1014+
for index, outspec in enumerate(outlist or []):
9991015
if isinstance(outspec, (int, str, tuple)):
10001016
outspec = [outspec]
10011017
if not isinstance(outspec, list):
@@ -1009,13 +1025,14 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
10091025
self.output_map[index, ylist_index] += gain
10101026

10111027
# Save the parameters for the system
1012-
self.params = params.copy()
1028+
self.params = {} if params is None else params.copy()
10131029

10141030
def _update_params(self, params, warning=False):
10151031
for sys in self.syslist:
10161032
local = sys.params.copy() # start with system parameters
10171033
local.update(self.params) # update with global params
1018-
local.update(params) # update with locally passed parameters
1034+
if params:
1035+
local.update(params) # update with locally passed parameters
10191036
sys._update_params(local, warning=warning)
10201037

10211038
def _rhs(self, t, x, u):
@@ -1565,7 +1582,7 @@ def __init__(self, io_sys, ss_sys=None):
15651582

15661583

15671584
def input_output_response(
1568-
sys, T, U=0., X0=0, params={},
1585+
sys, T, U=0., X0=0, params=None,
15691586
transpose=False, return_x=False, squeeze=None,
15701587
solve_ivp_kwargs={}, t_eval='T', **kwargs):
15711588
"""Compute the output response of a system to a given input.
@@ -1781,7 +1798,7 @@ def input_output_response(
17811798

17821799
# Update the parameter values
17831800
sys._update_params(params)
1784-
1801+
17851802
#
17861803
# Define a function to evaluate the input at an arbitrary time
17871804
#
@@ -1900,7 +1917,7 @@ def ivp_rhs(t, x):
19001917
transpose=transpose, return_x=return_x, squeeze=squeeze)
19011918

19021919

1903-
def find_eqpt(sys, x0, u0=[], y0=None, t=0, params={},
1920+
def find_eqpt(sys, x0, u0=None, y0=None, t=0, params=None,
19041921
iu=None, iy=None, ix=None, idx=None, dx0=None,
19051922
return_y=False, return_result=False):
19061923
"""Find the equilibrium point for an input/output system.
@@ -2151,7 +2168,7 @@ def rootfun(z):
21512168

21522169

21532170
# Linearize an input/output system
2154-
def linearize(sys, xeq, ueq=[], t=0, params={}, **kw):
2171+
def linearize(sys, xeq, ueq=None, t=0, params=None, **kw):
21552172
"""Linearize an input/output system at a given state and input.
21562173
21572174
This function computes the linearization of an input/output system at a
@@ -2242,7 +2259,7 @@ def ss(*args, **kwargs):
22422259
Convert a linear system into space system form. Always creates a
22432260
new system, even if sys is already a state space system.
22442261
2245-
``ss(updfcn, outfucn)``
2262+
``ss(updfcn, outfcn)``
22462263
Create a nonlinear input/output system with update function ``updfcn``
22472264
and output function ``outfcn``. See :class:`NonlinearIOSystem` for
22482265
more information.
@@ -2523,9 +2540,9 @@ def tf2io(*args, **kwargs):
25232540

25242541

25252542
# Function to create an interconnected system
2526-
def interconnect(syslist, connections=None, inplist=[], outlist=[], params={},
2527-
check_unused=True, ignore_inputs=None, ignore_outputs=None,
2528-
warn_duplicate=None, **kwargs):
2543+
def interconnect(syslist, connections=None, inplist=None, outlist=None,
2544+
params=None, check_unused=True, ignore_inputs=None,
2545+
ignore_outputs=None, warn_duplicate=None, **kwargs):
25292546
"""Interconnect a set of input/output systems.
25302547
25312548
This function creates a new system that is an interconnection of a set of
@@ -2767,10 +2784,10 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], params={},
27672784
connections = []
27682785

27692786
# If inplist/outlist is not present, try using inputs/outputs instead
2770-
if not inplist and inputs is not None:
2771-
inplist = list(inputs)
2772-
if not outlist and outputs is not None:
2773-
outlist = list(outputs)
2787+
if inplist is None:
2788+
inplist = list(inputs or [])
2789+
if outlist is None:
2790+
outlist = list(outputs or [])
27742791

27752792
# Process input list
27762793
if not isinstance(inplist, (list, tuple)):

control/statesp.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -1388,7 +1388,7 @@ def dcgain(self, warn_infinite=False):
13881388
"""
13891389
return self._dcgain(warn_infinite)
13901390

1391-
def dynamics(self, t, x, u=None):
1391+
def dynamics(self, t, x, u=None, params=None):
13921392
"""Compute the dynamics of the system
13931393
13941394
Given input `u` and state `x`, returns the dynamics of the state-space
@@ -1422,6 +1422,9 @@ def dynamics(self, t, x, u=None):
14221422
dx/dt or x[t+dt] : ndarray
14231423
14241424
"""
1425+
if params is not None:
1426+
warn("params keyword ignored for StateSpace object")
1427+
14251428
x = np.reshape(x, (-1, 1)) # force to a column in case matrix
14261429
if np.size(x) != self.nstates:
14271430
raise ValueError("len(x) must be equal to number of states")
@@ -1434,7 +1437,7 @@ def dynamics(self, t, x, u=None):
14341437
return (self.A @ x).reshape((-1,)) \
14351438
+ (self.B @ u).reshape((-1,)) # return as row vector
14361439

1437-
def output(self, t, x, u=None):
1440+
def output(self, t, x, u=None, params=None):
14381441
"""Compute the output of the system
14391442
14401443
Given input `u` and state `x`, returns the output `y` of the
@@ -1464,6 +1467,9 @@ def output(self, t, x, u=None):
14641467
-------
14651468
y : ndarray
14661469
"""
1470+
if params is not None:
1471+
warn("params keyword ignored for StateSpace object")
1472+
14671473
x = np.reshape(x, (-1, 1)) # force to a column in case matrix
14681474
if np.size(x) != self.nstates:
14691475
raise ValueError("len(x) must be equal to number of states")

control/tests/statesp_test.py

+11
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,7 @@ def test_linfnorm_ct_mimo(self, ct_siso):
11401140
np.testing.assert_allclose(gpeak, refgpeak)
11411141
np.testing.assert_allclose(fpeak, reffpeak)
11421142

1143+
11431144
@pytest.mark.parametrize("args, static", [
11441145
(([], [], [], 1), True), # ctime, empty state
11451146
(([], [], [], 1, 1), True), # dtime, empty state
@@ -1153,3 +1154,13 @@ def test_linfnorm_ct_mimo(self, ct_siso):
11531154
def test_isstatic(args, static):
11541155
sys = ct.StateSpace(*args)
11551156
assert sys._isstatic() == static
1157+
1158+
# Make sure that using params for StateSpace objects generates a warning
1159+
def test_params_warning():
1160+
sys = StateSpace(-1, 1, 1, 0)
1161+
1162+
with pytest.warns(UserWarning, match="params keyword ignored"):
1163+
sys.dynamics(0, [0], [0], {'k': 5})
1164+
1165+
with pytest.warns(UserWarning, match="params keyword ignored"):
1166+
sys.output(0, [0], [0], {'k': 5})

0 commit comments

Comments
 (0)