@@ -1571,7 +1571,7 @@ def __init__(self, io_sys, ss_sys=None):
15711571def 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.
15761576
15771577 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' )
16581658
16591659 # Set the default method to 'RK45'
16601660 if solve_ivp_kwargs .get ('method' , None ) is None :
16611661 solve_ivp_kwargs ['method' ] = 'RK45'
16621662
1663+ # 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" )
16661670
16671671 # 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
16701685
16711686 # 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 ]
16941696
16951697 # 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 )
16981701
1699- # 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
17011708
17021709 #
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
17161723
1724+ # 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 )
17291763
1730- 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 ])
17421770
17431771 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." )
17491781
17501782 # Make sure the sample time matches the given time
@@ -1764,21 +1796,23 @@ def ivp_rhs(t, x):
17641796
17651797 # 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 ]))
17751808
17761809 # 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 ] )
17781811
17791812 # 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 ))
17821816
17831817 # 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" )
17881822
17891823 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 )
0 commit comments