Skip to content

Commit 85568a9

Browse files
committed
allow saving and passing of params (for nl systems) in time responses
1 parent e1e33e4 commit 85568a9

File tree

4 files changed

+84
-14
lines changed

4 files changed

+84
-14
lines changed

control/nlsys.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,8 @@ def input_output_response(
13671367
13681368
* inputs (array): Input(s) to the system, indexed by input and time.
13691369
1370+
* params (dict): Parameters values used for the simulation.
1371+
13701372
The return value of the system can also be accessed by assigning the
13711373
function to a tuple of length 2 (time, output) or of length 3 (time,
13721374
output, state) if ``return_x`` is ``True``. If the input/output
@@ -1643,7 +1645,7 @@ def ivp_rhs(t, x):
16431645
raise TypeError("Can't determine system type")
16441646

16451647
return TimeResponseData(
1646-
soln.t, y, soln.y, u, issiso=sys.issiso(),
1648+
soln.t, y, soln.y, u, params=params, issiso=sys.issiso(),
16471649
output_labels=sys.output_labels, input_labels=sys.input_labels,
16481650
state_labels=sys.state_labels, sysname=sys.name,
16491651
title="Input/output response for " + sys.name,

control/tests/nlsys_test.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,20 @@ def test_lti_nlsys_response(nin, nout, input, output):
7575
resp_nl = ct.forced_response(sys_nl, timepts, U, X0=X0)
7676
np.testing.assert_equal(resp_ss.time, resp_nl.time)
7777
np.testing.assert_allclose(resp_ss.states, resp_nl.states, atol=0.05)
78+
79+
80+
# Test to make sure that impulse responses are not allowed
81+
def test_nlsys_impulse():
82+
sys_ss = ct.rss(4, 1, 1, strictly_proper=True)
83+
sys_nl = ct.nlsys(
84+
lambda t, x, u, params: sys_ss.A @ x + sys_ss.B @ u,
85+
lambda t, x, u, params: sys_ss.C @ x + sys_ss.D @ u,
86+
inputs=1, outputs=1, states=4)
87+
88+
# Figure out the time to use from the linear impulse response
89+
resp_ss = ct.impulse_response(sys_ss)
90+
timepts = np.linspace(0, resp_ss.time[-1]/10, 100)
91+
92+
# Impulse_response (not implemented)
93+
with pytest.raises(ValueError, match="system must be LTI"):
94+
resp_nl = ct.impulse_response(sys_nl, timepts)

control/tests/trdata_test.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ def test_trdata_labels():
236236
np.testing.assert_equal(
237237
response.input_labels, ["u[%d]" % i for i in range(sys.ninputs)])
238238

239-
# Make sure the selected input and output are both correctly transferred to the response
239+
# Make sure the selected input and output are both correctly
240+
# transferred to the response
240241
for nu in range(sys.ninputs):
241242
for ny in range(sys.noutputs):
242243
step_response = ct.step_response(sys, T, input=nu, output=ny)
@@ -339,6 +340,37 @@ def test_trdata_multitrace():
339340
np.zeros(5), np.ones(5), np.zeros((1, 5)), np.zeros(6))
340341

341342

343+
@pytest.mark.parametrize("func, args", [
344+
(ct.step_response, ()),
345+
(ct.initial_response, (1, )),
346+
(ct.forced_response, (0, 1)),
347+
(ct.input_output_response, (0, 1)),
348+
])
349+
@pytest.mark.parametrize("dt", [0, 1])
350+
def test_trdata_params(func, args, dt):
351+
# Create a nonlinear system with parameters, neutrally stable
352+
nlsys = ct.nlsys(
353+
lambda t, x, u, params: params['a'] * x[0] + u[0],
354+
states = 1, inputs = 1, outputs = 1, params={'a': 0}, dt=dt)
355+
lnsys = ct.ss([[-0.5]], [[1]], [[1]], 0, dt=dt)
356+
357+
# Compute the response, setting parameters to make things stable
358+
timevec = np.linspace(0, 1) if dt == 0 else np.arange(0, 10, 1)
359+
nlresp = func(nlsys, timevec, *args, params={'a': -0.5})
360+
lnresp = func(lnsys, timevec, *args)
361+
362+
# Make sure the modified system was stable
363+
np.testing.assert_allclose(
364+
nlresp.states, lnresp.states, rtol=1e-3, atol=1e-5)
365+
assert lnresp.params == None
366+
assert nlresp.params['a'] == -0.5
367+
368+
# Make sure the match was not accidental
369+
bdresp = func(nlsys, timevec, *args)
370+
assert not np.allclose(
371+
bdresp.states, nlresp.states, rtol=1e-3, atol=1e-5)
372+
373+
342374
def test_trdata_exceptions():
343375
# Incorrect dimension for time vector
344376
with pytest.raises(ValueError, match="Time vector must be 1D"):

control/timeresp.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ class TimeResponseData:
173173
sysname : str, optional
174174
Name of the system that created the data.
175175
176+
params : dict, optional
177+
If system is a nonlinear I/O system, set parameter values.
178+
176179
plot_inputs : bool, optional
177180
Whether or not to plot the inputs by default (can be overridden in
178181
the plot() method)
@@ -227,7 +230,7 @@ def __init__(
227230
output_labels=None, state_labels=None, input_labels=None,
228231
title=None, transpose=False, return_x=False, squeeze=None,
229232
multi_trace=False, trace_labels=None, trace_types=None,
230-
plot_inputs=True, sysname=None
233+
plot_inputs=True, sysname=None, params=None
231234
):
232235
"""Create an input/output time response object.
233236
@@ -315,6 +318,7 @@ def __init__(
315318
raise ValueError("Time vector must be 1D array")
316319
self.title = title
317320
self.sysname = sysname
321+
self.params = params
318322

319323
#
320324
# Output vector (and number of traces)
@@ -856,7 +860,7 @@ def shape_matches(s_legal, s_actual):
856860

857861

858862
# Forced response of a linear system
859-
def forced_response(sys, T=None, U=0., X0=0., transpose=False,
863+
def forced_response(sys, T=None, U=0., X0=0., transpose=False, params=None,
860864
interpolate=False, return_x=None, squeeze=None):
861865
"""Compute the output of a linear system given the input.
862866
@@ -889,6 +893,9 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False,
889893
X0 : array_like or float, default=0.
890894
Initial condition.
891895
896+
params : dict, optional
897+
If system is a nonlinear I/O system, set parameter values.
898+
892899
transpose : bool, default=False
893900
If True, transpose all input and output arrays (for backward
894901
compatibility with MATLAB and :func:`scipy.signal.lsim`).
@@ -981,7 +988,7 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False,
981988
warnings.warn(
982989
"interpolation not supported for nonlinear I/O systems")
983990
return input_output_response(
984-
sys, T, U, X0, transpose=transpose,
991+
sys, T, U, X0, params=params, transpose=transpose,
985992
return_x=return_x, squeeze=squeeze)
986993
else:
987994
raise TypeError('Parameter ``sys``: must be a ``StateSpace`` or'
@@ -1169,7 +1176,7 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False,
11691176
yout = np.transpose(yout)
11701177

11711178
return TimeResponseData(
1172-
tout, yout, xout, U, issiso=sys.issiso(),
1179+
tout, yout, xout, U, params=params, issiso=sys.issiso(),
11731180
output_labels=sys.output_labels, input_labels=sys.input_labels,
11741181
state_labels=sys.state_labels, sysname=sys.name, plot_inputs=True,
11751182
title="Forced response for " + sys.name, trace_types=['forced'],
@@ -1256,7 +1263,7 @@ def _process_time_response(
12561263

12571264

12581265
def step_response(sys, T=None, X0=0, input=None, output=None, T_num=None,
1259-
transpose=False, return_x=False, squeeze=None):
1266+
transpose=False, return_x=False, squeeze=None, params=None):
12601267
# pylint: disable=W0622
12611268
"""Compute the step response for a linear system.
12621269
@@ -1298,6 +1305,9 @@ def step_response(sys, T=None, X0=0, input=None, output=None, T_num=None,
12981305
Only report the step response for the listed output. If not
12991306
specified, all outputs are reported.
13001307
1308+
params : dict, optional
1309+
If system is a nonlinear I/O system, set parameter values.
1310+
13011311
T_num : int, optional
13021312
Number of time steps to use in simulation if T is not provided as an
13031313
array (autocomputed if not given); ignored if sys is discrete-time.
@@ -1403,7 +1413,7 @@ def step_response(sys, T=None, X0=0, input=None, output=None, T_num=None,
14031413
U = np.zeros((sys.ninputs, T.size))
14041414
U[i, :] = np.ones_like(T)
14051415

1406-
response = forced_response(sys, T, U, X0, squeeze=True)
1416+
response = forced_response(sys, T, U, X0, squeeze=True, params=params)
14071417
inpidx = i if input is None else 0
14081418
yout[:, inpidx, :] = response.y if output is None \
14091419
else response.y[output]
@@ -1424,11 +1434,11 @@ def step_response(sys, T=None, X0=0, input=None, output=None, T_num=None,
14241434
output_labels=output_labels, input_labels=input_labels,
14251435
state_labels=sys.state_labels, title="Step response for " + sys.name,
14261436
transpose=transpose, return_x=return_x, squeeze=squeeze,
1427-
sysname=sys.name, trace_labels=trace_labels,
1437+
sysname=sys.name, params=params, trace_labels=trace_labels,
14281438
trace_types=trace_types, plot_inputs=False)
14291439

14301440

1431-
def step_info(sysdata, T=None, T_num=None, yfinal=None,
1441+
def step_info(sysdata, T=None, T_num=None, yfinal=None, params=None,
14321442
SettlingTimeThreshold=0.02, RiseTimeLimits=(0.1, 0.9)):
14331443
"""
14341444
Step response characteristics (Rise time, Settling Time, Peak and others).
@@ -1451,6 +1461,8 @@ def step_info(sysdata, T=None, T_num=None, yfinal=None,
14511461
systems to simulate and the last value of the the response data is
14521462
used for a given time series of response data. Scalar for SISO,
14531463
(noutputs, ninputs) array_like for MIMO systems.
1464+
params : dict, optional
1465+
If system is a nonlinear I/O system, set parameter values.
14541466
SettlingTimeThreshold : float, optional
14551467
Defines the error to compute settling time (default = 0.02)
14561468
RiseTimeLimits : tuple (lower_threshold, upper_theshold)
@@ -1536,7 +1548,7 @@ def step_info(sysdata, T=None, T_num=None, yfinal=None,
15361548
from .nlsys import NonlinearIOSystem
15371549

15381550
if isinstance(sysdata, (StateSpace, TransferFunction, NonlinearIOSystem)):
1539-
T, Yout = step_response(sysdata, T, squeeze=False)
1551+
T, Yout = step_response(sysdata, T, squeeze=False, params=params)
15401552
if yfinal:
15411553
InfValues = np.atleast_2d(yfinal)
15421554
else:
@@ -1651,7 +1663,7 @@ def step_info(sysdata, T=None, T_num=None, yfinal=None,
16511663
return ret[0][0] if retsiso else ret
16521664

16531665

1654-
def initial_response(sys, T=None, X0=0, output=None, T_num=None,
1666+
def initial_response(sys, T=None, X0=0, output=None, T_num=None, params=None,
16551667
transpose=False, return_x=False, squeeze=None):
16561668
# pylint: disable=W0622
16571669
"""Compute the initial condition response for a linear system.
@@ -1684,6 +1696,9 @@ def initial_response(sys, T=None, X0=0, output=None, T_num=None,
16841696
Number of time steps to use in simulation if T is not provided as an
16851697
array (autocomputed if not given); ignored if sys is discrete-time.
16861698
1699+
params : dict, optional
1700+
If system is a nonlinear I/O system, set parameter values.
1701+
16871702
transpose : bool, optional
16881703
If True, transpose all input and output arrays (for backward
16891704
compatibility with MATLAB and :func:`scipy.signal.lsim`). Default
@@ -1748,7 +1763,7 @@ def initial_response(sys, T=None, X0=0, output=None, T_num=None,
17481763
raise ValueError("invalid value of T for this type of system")
17491764

17501765
# Compute the forced response
1751-
response = forced_response(sys, T, 0, X0)
1766+
response = forced_response(sys, T, 0, X0, params=params)
17521767

17531768
# Figure out if the system is SISO or not
17541769
issiso = sys.issiso() or output is not None
@@ -1760,7 +1775,7 @@ def initial_response(sys, T=None, X0=0, output=None, T_num=None,
17601775

17611776
# Store the response without an input
17621777
return TimeResponseData(
1763-
response.t, yout, response.x, None, issiso=issiso,
1778+
response.t, yout, response.x, None, params=params, issiso=issiso,
17641779
output_labels=output_labels, input_labels=None,
17651780
state_labels=sys.state_labels, sysname=sys.name,
17661781
title="Initial response for " + sys.name, trace_types=['initial'],
@@ -1863,6 +1878,10 @@ def impulse_response(sys, T=None, input=None, output=None, T_num=None,
18631878
from .statesp import _convert_to_statespace
18641879
from .lti import LTI
18651880

1881+
# Make sure we have an LTI system
1882+
if not isinstance(sys, LTI):
1883+
raise ValueError("system must be LTI system for impulse response")
1884+
18661885
# Create the time and input vectors
18671886
if T is None or np.asarray(T).size == 1:
18681887
T = _default_time_vector(sys, N=T_num, tfinal=T, is_step=False)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy