1818
1919"""
2020
21- import numpy as np
22- import scipy as sp
2321import copy
2422from warnings import warn
2523
24+ import numpy as np
25+ import scipy as sp
26+
2627from . import config
27- from .iosys import InputOutputSystem , _process_signal_list , \
28- _process_iosys_keywords , isctime , isdtime , common_timebase , _parse_spec
29- from .timeresp import _check_convert_array , _process_time_response , \
30- TimeResponseData
28+ from .iosys import InputOutputSystem , _parse_spec , _process_iosys_keywords , \
29+ _process_signal_list , common_timebase , isctime , isdtime
30+ from .timeresp import TimeResponseData , _check_convert_array , \
31+ _process_time_response
3132
3233__all__ = ['NonlinearIOSystem' , 'InterconnectedSystem' , 'nlsys' ,
3334 'input_output_response' , 'find_eqpt' , 'linearize' ,
@@ -132,13 +133,15 @@ def __init__(self, updfcn, outfcn=None, params=None, **kwargs):
132133 if updfcn is None :
133134 if self .nstates is None :
134135 self .nstates = 0
136+ self .updfcn = lambda t , x , u , params : np .zeros (0 )
135137 else :
136138 raise ValueError (
137139 "states specified but no update function given." )
138140
139141 if outfcn is None :
140- # No output function specified => outputs = states
141- if self .noutputs is None and self .nstates is not None :
142+ if self .noutputs == 0 :
143+ self .outfcn = lambda t , x , u , params : np .zeros (0 )
144+ elif self .noutputs is None and self .nstates is not None :
142145 self .noutputs = self .nstates
143146 elif self .noutputs is not None and self .noutputs == self .nstates :
144147 # Number of outputs = number of states => all is OK
@@ -364,9 +367,8 @@ def _rhs(self, t, x, u):
364367 user-friendly interface you may want to use :meth:`dynamics`.
365368
366369 """
367- xdot = self .updfcn (t , x , u , self ._current_params ) \
368- if self .updfcn is not None else []
369- return np .array (xdot ).reshape ((- 1 ,))
370+ return np .asarray (
371+ self .updfcn (t , x , u , self ._current_params )).reshape (- 1 )
370372
371373 def dynamics (self , t , x , u , params = None ):
372374 """Compute the dynamics of a differential or difference equation.
@@ -403,7 +405,8 @@ def dynamics(self, t, x, u, params=None):
403405 dx/dt or x[t+dt] : ndarray
404406 """
405407 self ._update_params (params )
406- return self ._rhs (t , x , u )
408+ return self ._rhs (
409+ t , np .asarray (x ).reshape (- 1 ), np .asarray (u ).reshape (- 1 ))
407410
408411 def _out (self , t , x , u ):
409412 """Evaluate the output of a system at a given state, input, and time
@@ -414,9 +417,17 @@ def _out(self, t, x, u):
414417 :meth:`output`.
415418
416419 """
417- y = self .outfcn (t , x , u , self ._current_params ) \
418- if self .outfcn is not None else x
419- return np .array (y ).reshape ((- 1 ,))
420+ #
421+ # To allow lazy evaluation of the system size, we allow for the
422+ # possibility that noutputs is left unspecified when the system
423+ # is created => we have to check for that case here (and return
424+ # the system state or a portion of it).
425+ #
426+ if self .outfcn is None :
427+ return x if self .noutputs is None else x [:self .noutputs ]
428+ else :
429+ return np .asarray (
430+ self .outfcn (t , x , u , self ._current_params )).reshape (- 1 )
420431
421432 def output (self , t , x , u , params = None ):
422433 """Compute the output of the system
@@ -444,7 +455,8 @@ def output(self, t, x, u, params=None):
444455 y : ndarray
445456 """
446457 self ._update_params (params )
447- return self ._out (t , x , u )
458+ return self ._out (
459+ t , np .asarray (x ).reshape (- 1 ), np .asarray (u ).reshape (- 1 ))
448460
449461 def feedback (self , other = 1 , sign = - 1 , params = None ):
450462 """Feedback interconnection between two input/output systems
@@ -517,14 +529,13 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
517529 # numerical linearization use the `_rhs()` and `_out()` member
518530 # functions.
519531 #
520-
521532 # If x0 and u0 are specified as lists, concatenate the elements
522533 x0 = _concatenate_list_elements (x0 , 'x0' )
523534 u0 = _concatenate_list_elements (u0 , 'u0' )
524535
525536 # Figure out dimensions if they were not specified.
<
38BA
/td>526- nstates = _find_size (self .nstates , x0 )
527- ninputs = _find_size (self .ninputs , u0 )
537+ nstates = _find_size (self .nstates , x0 , "states" )
538+ ninputs = _find_size (self .ninputs , u0 , "inputs" )
528539
529540 # Convert x0, u0 to arrays, if needed
530541 if np .isscalar (x0 ):
@@ -533,7 +544,7 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
533544 u0 = np .ones ((ninputs ,)) * u0
534545
535546 # Compute number of outputs by evaluating the output function
536- noutputs = _find_size (self .noutputs , self ._out (t , x0 , u0 ))
547+ noutputs = _find_size (self .noutputs , self ._out (t , x0 , u0 ), "outputs" )
537548
538549 # Update the current parameters
539550 self ._update_params (params )
@@ -1306,7 +1317,7 @@ def nlsys(
13061317
13071318
13081319def input_output_response (
1309- sys , T , U = 0. , X0 = 0 , params = None ,
1320+ sys , T , U = 0. , X0 = 0 , params = None , ignore_errors = False ,
13101321 transpose = False , return_x = False , squeeze = None ,
13111322 solve_ivp_kwargs = None , t_eval = 'T' , ** kwargs ):
13121323 """Compute the output response of a system to a given input.
@@ -1382,6 +1393,11 @@ def input_output_response(
13821393 to 'RK45'.
13831394 solve_ivp_kwargs : dict, optional
13841395 Pass additional keywords to :func:`scipy.integrate.solve_ivp`.
1396+ ignore_errors : bool, optional
1397+ If ``False`` (default), errors during computation of the trajectory
1398+ will raise a ``RuntimeError`` exception. If ``True``, do not raise
1399+ an exception and instead set ``results.success`` to ``False`` and
1400+ place an error message in ``results.message``.
13851401
13861402 Raises
13871403 ------
@@ -1516,7 +1532,7 @@ def input_output_response(
15161532 X0 = np .hstack ([X0 , np .zeros (sys .nstates - X0 .size )])
15171533
15181534 # Compute the number of states
1519- nstates = _find_size (sys .nstates , X0 )
1535+ nstates = _find_size (sys .nstates , X0 , "states" )
15201536
15211537 # create X0 if not given, test if X0 has correct shape
15221538 X0 = _check_convert_array (X0 , [(nstates ,), (nstates , 1 )],
@@ -1583,7 +1599,11 @@ def ivp_rhs(t, x):
15831599 ivp_rhs , (T0 , Tf ), X0 , t_eval = t_eval ,
15841600 vectorized = False , ** solve_ivp_kwargs )
15851601 if not soln .success :
1586- raise RuntimeError ("solve_ivp failed: " + soln .message )
1602+ message = "solve_ivp failed: " + soln .message
1603+ if not ignore_errors :
1604+ raise RuntimeError (message )
1605+ else :
1606+ message = None
15871607
15881608 # Compute inputs and outputs for each time point
15891609 u = np .zeros ((ninputs , len (soln .t )))
@@ -1639,7 +1659,7 @@ def ivp_rhs(t, x):
16391659 u = np .transpose (np .array (u ))
16401660
16411661 # Mark solution as successful
1642- soln .success = True # No way to fail
1662+ soln .success , message = True , None # No way to fail
16431663
16441664 else : # Neither ctime or dtime??
16451665 raise TypeError ("Can't determine system type" )
@@ -1649,7 +1669,8 @@ def ivp_rhs(t, x):
16491669 output_labels = sys .output_labels , input_labels = sys .input_labels ,
16501670 state_labels = sys .state_labels , sysname = sys .name ,
16511671 title = "Input/output response for " + sys .name ,
1652- transpose = transpose , return_x = return_x , squeeze = squeeze )
1672+ transpose = transpose , return_x = return_x , squeeze = squeeze ,
1673+ success = soln .success , message = message )
16531674
16541675
16551676def find_eqpt (sys , x0 , u0 = None , y0 = None , t = 0 , params = None ,
@@ -1732,9 +1753,9 @@ def find_eqpt(sys, x0, u0=None, y0=None, t=0, params=None,
17321753 from scipy .optimize import root
17331754
17341755 # Figure out the number of states, inputs, and outputs
1735- nstates = _find_size (sys .nstates , x0 )
1736- ninputs = _find_size (sys .ninputs , u0 )
1737- noutputs = _find_size (sys .noutputs , y0 )
1756+ nstates = _find_size (sys .nstates , x0 , "states" )
1757+ ninputs = _find_size (sys .ninputs , u0 , "inputs" )
1758+ noutputs = _find_size (sys .noutputs , y0 , "outputs" )
17381759
17391760 # Convert x0, u0, y0 to arrays, if needed
17401761 if np .isscalar (x0 ):
@@ -1977,23 +1998,23 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw):
19771998 return sys .linearize (xeq , ueq , t = t , params = params , ** kw )
19781999
19792000
1980- def _find_size (sysval , vecval ):
2001+ def _find_size (sysval , vecval , label ):
19812002 """Utility function to find the size of a system parameter
D306
td>19822003
19832004 If both parameters are not None, they must be consistent.
19842005 """
19852006 if hasattr (vecval , '__len__' ):
19862007 if sysval is not None and sysval != len (vecval ):
1987- raise ValueError ("Inconsistent information to determine size "
1988- " of system component " )
2008+ raise ValueError (
2009+ f"inconsistent information for number of { label } " )
19892010 return len (vecval )
19902011 # None or 0, which is a valid value for "a (sysval, ) vector of zeros".
19912012 if not vecval :
19922013 return 0 if sysval is None else sysval
19932014 elif sysval == 1 :
19942015 # (1, scalar) is also a valid combination from legacy code
19952016 return 1
1996- raise ValueError ("can't determine size of system component " )
2017+ raise ValueError (f "can't determine number of { label } " )
19972018
19982019
19992020# Function to create an interconnected system
@@ -2241,7 +2262,7 @@ def interconnect(
22412262 `outputs`, for more natural naming of SISO systems.
22422263
22432264 """
2244- from .statesp import StateSpace , LinearICSystem , _convert_to_statespace
2265+ from .statesp import LinearICSystem , StateSpace , _convert_to_statespace
22452266 from .xferfcn import TransferFunction
22462267
22472268 dt = kwargs .pop ('dt' , None ) # bypass normal 'dt' processing
@@ -2551,7 +2572,7 @@ def interconnect(
25512572 return newsys
25522573
25532574
2554- # Utility function to allow lists states, inputs
2575+ # Utility function to allow lists of states, inputs
25552576def _concatenate_list_elements (X , name = 'X' ):
25562577 # If we were passed a list, concatenate the elements together
25572578 if isinstance (X , (tuple , list )):
@@ -2574,13 +2595,14 @@ def _convert_static_iosystem(sys):
25742595 # Convert sys1 to an I/O system if needed
25752596 if isinstance (sys , (int , float , np .number )):
25762597 return NonlinearIOSystem (
2577- None , lambda t , x , u , params : sys * u , inputs = 1 , outputs = 1 )
2598+ None , lambda t , x , u , params : sys * u ,
2599+ outputs = 1 , inputs = 1 , dt = None )
25782600
25792601 elif isinstance (sys , np .ndarray ):
25802602 sys = np .atleast_2d (sys )
25812603 return NonlinearIOSystem (
25822604 None , lambda t , x , u , params : sys @ u ,
2583- outputs = sys .shape [0 ], inputs = sys .shape [1 ])
2605+ outputs = sys .shape [0 ], inputs = sys .shape [1 ], dt = None )
25842606
25852607def connection_table (sys , show_names = False , column_width = 32 ):
25862608 """Print table of connections inside an interconnected system model.
0 commit comments