Merge pull request #712 from murrayrm/optimal_18Mar2022 · python-control/python-control@cfe21de
@@ -1571,7 +1571,7 @@ def __init__(self, io_sys, ss_sys=None):
15711571def input_output_response(
15721572sys, T, U=0., X0=0, params={},
15731573transpose=False, return_x=False, squeeze=None,
1574-solve_ivp_kwargs={}, **kwargs):
1574+solve_ivp_kwargs={}, t_eval='T', **kwargs):
15751575"""Compute the output response of a system to a given input.
1576157615771577 Simulate a dynamical system with a given input and return its output
@@ -1654,50 +1654,57 @@ def input_output_response(
16541654if kwargs.get('solve_ivp_method', None):
16551655if kwargs.get('method', None):
16561656raise ValueError("ivp_method specified more than once")
1657-solve_ivp_kwargs['method'] = kwargs['solve_ivp_method']
1657+solve_ivp_kwargs['method'] = kwargs.pop('solve_ivp_method')
1658165816591659# Set the default method to 'RK45'
16601660if solve_ivp_kwargs.get('method', None) is None:
16611661solve_ivp_kwargs['method'] = 'RK45'
166216621663+# Make sure all input arguments got parsed
1664+if kwargs:
1665+raise TypeError("unknown parameters %s" % kwargs)
1666+16631667# Sanity checking on the input
16641668if not isinstance(sys, InputOutputSystem):
16651669raise TypeError("System of type ", type(sys), " not valid")
1666167016671671# Compute the time interval and number of steps
16681672T0, Tf = T[0], T[-1]
1669-n_steps = len(T)
1673+ntimepts = len(T)
1674+1675+# Figure out simulation times (t_eval)
1676+if solve_ivp_kwargs.get('t_eval'):
1677+if t_eval == 'T':
1678+# Override the default with the solve_ivp keyword
1679+t_eval = solve_ivp_kwargs.pop('t_eval')
1680+else:
1681+raise ValueError("t_eval specified more than once")
1682+if isinstance(t_eval, str) and t_eval == 'T':
1683+# Use the input time points as the output time points
1684+t_eval = T
1670168516711686# Check and convert the input, if needed
16721687# TODO: improve MIMO ninputs check (choose from U)
16731688if sys.ninputs is None or sys.ninputs == 1:
1674-legal_shapes = [(n_steps,), (1, n_steps)]
1689+legal_shapes = [(ntimepts,), (1, ntimepts)]
16751690else:
1676-legal_shapes = [(sys.ninputs, n_steps)]
1691+legal_shapes = [(sys.ninputs, ntimepts)]
16771692U = _check_convert_array(U, legal_shapes,
16781693'Parameter ``U``: ', squeeze=False)
1679-U = U.reshape(-1, n_steps)
1680-1681-# Check to make sure this is not a static function
1682-nstates = _find_size(sys.nstates, X0)
1683-if nstates == 0:
1684-# No states => map input to output
1685-u = U[0] if len(U.shape) == 1 else U[:, 0]
1686-y = np.zeros((np.shape(sys._out(T[0], X0, u))[0], len(T)))
1687-for i in range(len(T)):
1688-u = U[i] if len(U.shape) == 1 else U[:, i]
1689-y[:, i] = sys._out(T[i], [], u)
1690-return TimeResponseData(
1691-T, y, None, U, issiso=sys.issiso(),
1692-output_labels=sys.output_index, input_labels=sys.input_index,
1693-transpose=transpose, return_x=return_x, squeeze=squeeze)
1694+U = U.reshape(-1, ntimepts)
1695+ninputs = U.shape[0]
1694169616951697# create X0 if not given, test if X0 has correct shape
1698+nstates = _find_size(sys.nstates, X0)
16961699X0 = _check_convert_array(X0, [(nstates,), (nstates, 1)],
16971700'Parameter ``X0``: ', squeeze=True)
169817011699-# Update the parameter values
1700-sys._update_params(params)
1702+# Figure out the number of outputs
1703+if sys.noutputs is None:
1704+# Evaluate the output function to find number of outputs
1705+noutputs = np.shape(sys._out(T[0], X0, U[:, 0]))[0]
1706+else:
1707+noutputs = sys.noutputs
1701170817021709#
17031710# Define a function to evaluate the input at an arbitrary time
@@ -1714,6 +1721,31 @@ def ufun(t):
17141721dt = (t - T[idx-1]) / (T[idx] - T[idx-1])
17151722return U[..., idx-1] * (1. - dt) + U[..., idx] * dt
171617231724+# Check to make sure this is not a static function
1725+if nstates == 0: # No states => map input to output
1726+# Make sure the user gave a time vector for evaluation (or 'T')
1727+if t_eval is None:
1728+# User overrode t_eval with None, but didn't give us the times...
1729+warn("t_eval set to None, but no dynamics; using T instead")
1730+t_eval = T
1731+1732+# Allocate space for the inputs and outputs
1733+u = np.zeros((ninputs, len(t_eval)))
1734+y = np.zeros((noutputs, len(t_eval)))
1735+1736+# Compute the input and output at each point in time
1737+for i, t in enumerate(t_eval):
1738+u[:, i] = ufun(t)
1739+y[:, i] = sys._out(t, [], u[:, i])
1740+1741+return TimeResponseData(
1742+t_eval, y, None, u, issiso=sys.issiso(),
1743+output_labels=sys.output_index, input_labels=sys.input_index,
1744+transpose=transpose, return_x=return_x, squeeze=squeeze)
1745+1746+# Update the parameter values
1747+sys._update_params(params)
1748+17171749# Create a lambda function for the right hand side
17181750def ivp_rhs(t, x):
17191751return sys._rhs(t, x, ufun(t))
@@ -1724,27 +1756,27 @@ def ivp_rhs(t, x):
17241756raise NameError("scipy.integrate.solve_ivp not found; "
17251757"use SciPy 1.0 or greater")
17261758soln = sp.integrate.solve_ivp(
1727-ivp_rhs, (T0, Tf), X0, t_eval=T,
1759+ivp_rhs, (T0, Tf), X0, t_eval=t_eval,
17281760vectorized=False, **solve_ivp_kwargs)
1761+if not soln.success:
1762+raise RuntimeError("solve_ivp failed: " + soln.message)
172917631730-if not soln.success or soln.status != 0:
1731-# Something went wrong
1732-warn("sp.integrate.solve_ivp failed")
1733-print("Return bunch:", soln)
1734-1735-# Compute the output associated with the state (and use sys.out to
1736-# figure out the number of outputs just in case it wasn't specified)
1737-u = U[0] if len(U.shape) == 1 else U[:, 0]
1738-y = np.zeros((np.shape(sys._out(T[0], X0, u))[0], len(T)))
1739-for i in range(len(T)):
1740-u = U[i] if len(U.shape) == 1 else U[:, i]
1741-y[:, i] = sys._out(T[i], soln.y[:, i], u)
1764+# Compute inputs and outputs for each time point
1765+u = np.zeros((ninputs, len(soln.t)))
1766+y = np.zeros((noutputs, len(soln.t)))
1767+for i, t in enumerate(soln.t):
1768+u[:, i] = ufun(t)
1769+y[:, i] = sys._out(t, soln.y[:, i], u[:, i])
1742177017431771elif isdtime(sys):
1772+# If t_eval was not specified, use the sampling time
1773+if t_eval is None:
1774+t_eval = np.arange(T[0], T[1] + sys.dt, sys.dt)
1775+17441776# Make sure the time vector is uniformly spaced
1745-dt = T[1] - T[0]
1746-if not np.allclose(T[1:] - T[:-1], dt):
1747-raise ValueError("Parameter ``T``: time values must be "
1777+dt = t_eval[1] - t_eval[0]
1778+if not np.allclose(t_eval[1:] - t_eval[:-1], dt):
1779+raise ValueError("Parameter ``t_eval``: time values must be "
17481780"equally spaced.")
1749178117501782# Make sure the sample time matches the given time
@@ -1764,21 +1796,23 @@ def ivp_rhs(t, x):
1764179617651797# Compute the solution
17661798soln = sp.optimize.OptimizeResult()
1767-soln.t = T # Store the time vector directly
1768-x = X0 # Initilize state
1799+soln.t = t_eval # Store the time vector directly
1800+x = np.array(X0) # State vector (store as floats)
17691801soln.y = [] # Solution, following scipy convention
1770-y = [] # System output
1771-for i in range(len(T)):
1772-# Store the current state and output
1802+u, y = [], [] # System input, output
1803+for t in t_eval:
1804+# Store the current input, state, and output
17731805soln.y.append(x)
1774-y.append(sys._out(T[i], x, ufun(T[i])))
1806+u.append(ufun(t))
1807+y.append(sys._out(t, x, u[-1]))
1775180817761809# Update the state for the next iteration
1777-x = sys._rhs(T[i], x, ufun(T[i]))
1810+x = sys._rhs(t, x, u[-1])
1778181117791812# Convert output to numpy arrays
17801813soln.y = np.transpose(np.array(soln.y))
17811814y = np.transpose(np.array(y))
1815+u = np.transpose(np.array(u))
1782181617831817# Mark solution as successful
17841818soln.success = True # No way to fail
@@ -1787,7 +1821,7 @@ def ivp_rhs(t, x):
17871821raise TypeError("Can't determine system type")
1788182217891823return TimeResponseData(
1790-soln.t, y, soln.y, U, issiso=sys.issiso(),
1824+soln.t, y, soln.y, u, issiso=sys.issiso(),
17911825output_labels=sys.output_index, input_labels=sys.input_index,
17921826state_labels=sys.state_index,
17931827transpose=transpose, return_x=return_x, squeeze=squeeze)