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):

15711571

def input_output_response(

15721572

sys, T, U=0., X0=0, params={},

15731573

transpose=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(

16541654

if kwargs.get('solve_ivp_method', None):

16551655

if kwargs.get('method', None):

16561656

raise 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'

16601660

if solve_ivp_kwargs.get('method', None) is None:

16611661

solve_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

16641668

if not isinstance(sys, InputOutputSystem):

16651669

raise TypeError("System of type ", type(sys), " not valid")

1666167016671671

# Compute the time interval and number of steps

16681672

T0, 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)

16731688

if sys.ninputs is None or sys.ninputs == 1:

1674-

legal_shapes = [(n_steps,), (1, n_steps)]

1689+

legal_shapes = [(ntimepts,), (1, ntimepts)]

16751690

else:

1676-

legal_shapes = [(sys.ninputs, n_steps)]

1691+

legal_shapes = [(sys.ninputs, ntimepts)]

16771692

U = _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)

16961699

X0 = _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):

17141721

dt = (t - T[idx-1]) / (T[idx] - T[idx-1])

17151722

return 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

17181750

def ivp_rhs(t, x):

17191751

return sys._rhs(t, x, ufun(t))

@@ -1724,27 +1756,27 @@ def ivp_rhs(t, x):

17241756

raise NameError("scipy.integrate.solve_ivp not found; "

17251757

"use SciPy 1.0 or greater")

17261758

soln = sp.integrate.solve_ivp(

1727-

ivp_rhs, (T0, Tf), X0, t_eval=T,

1759+

ivp_rhs, (T0, Tf), X0, t_eval=t_eval,

17281760

vectorized=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])

1742177017431771

elif 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

17661798

soln = 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)

17691801

soln.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

17731805

soln.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

17801813

soln.y = np.transpose(np.array(soln.y))

17811814

y = np.transpose(np.array(y))

1815+

u = np.transpose(np.array(u))

1782181617831817

# Mark solution as successful

17841818

soln.success = True # No way to fail

@@ -1787,7 +1821,7 @@ def ivp_rhs(t, x):

17871821

raise TypeError("Can't determine system type")

1788182217891823

return TimeResponseData(

1790-

soln.t, y, soln.y, U, issiso=sys.issiso(),

1824+

soln.t, y, soln.y, u, issiso=sys.issiso(),

17911825

output_labels=sys.output_index, input_labels=sys.input_index,

17921826

state_labels=sys.state_index,

17931827

transpose=transpose, return_x=return_x, squeeze=squeeze)