18
18
19
19
"""
20
20
21
- import numpy as np
22
- import scipy as sp
23
21
import copy
24
22
from warnings import warn
25
23
24
+ import numpy as np
25
+ import scipy as sp
26
+
26
27
from .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
31
32
32
33
__all__ = ['NonlinearIOSystem' ,'InterconnectedSystem' ,'nlsys' ,
33
34
'input_output_response' ,'find_eqpt' ,'linearize' ,
@@ -132,13 +133,15 @@ def __init__(self, updfcn, outfcn=None, params=None, **kwargs):
132
133
if updfcn is None :
133
134
if self .nstates is None :
134
135
self .nstates = 0
136
+ self .updfcn = lambda t ,x ,u ,params :np .zeros (0 )
135
137
else :
136
138
raise ValueError (
137
139
"states specified but no update function given." )
138
140
139
141
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 :
142
145
self .noutputs = self .nstates
143
146
elif self .noutputs is not None and self .noutputs == self .nstates :
144
147
# Number of outputs = number of states => all is OK
@@ -364,9 +367,8 @@ def _rhs(self, t, x, u):
364
367
user-friendly interface you may want to use :meth:`dynamics`.
365
368
366
369
"""
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 )
370
372
371
373
def dynamics (self ,t ,x ,u ,params = None ):
372
374
"""Compute the dynamics of a differential or difference equation.
@@ -403,7 +405,8 @@ def dynamics(self, t, x, u, params=None):
403
405
dx/dt or x[t+dt] : ndarray
404
406
"""
405
407
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 ))
407
410
408
411
def _out (self ,t ,x ,u ):
409
412
"""Evaluate the output of a system at a given state, input, and time
@@ -414,9 +417,17 @@ def _out(self, t, x, u):
414
417
:meth:`output`.
415
418
416
419
"""
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 )
420
431
421
432
def output (self ,t ,x ,u ,params = None ):
422
433
"""Compute the output of the system
@@ -444,7 +455,8 @@ def output(self, t, x, u, params=None):
444
455
y : ndarray
445
456
"""
446
457
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 ))
448
460
449
461
def feedback (self ,other = 1 ,sign = - 1 ,params = None ):
450
462
"""Feedback interconnection between two input/output systems
@@ -517,14 +529,13 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
517
529
# numerical linearization use the `_rhs()` and `_out()` member
518
530
# functions.
519
531
#
520
-
521
532
# If x0 and u0 are specified as lists, concatenate the elements
522
533
x0 = _concatenate_list_elements (x0 ,'x0' )
523
534
u0 = _concatenate_list_elements (u0 ,'u0' )
524
535
525
536
# Figure out dimensions if they were not specified.
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" )
528
539
529
540
# Convert x0, u0 to arrays, if needed
530
541
if np .isscalar (x0 ):
@@ -533,7 +544,7 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
533
544
u0 = np .ones ((ninputs ,))* u0
534
545
535
546
# 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" )
537
548
538
549
# Update the current parameters
539
550
self ._update_params (params )
@@ -1306,7 +1317,7 @@ def nlsys(
1306
1317
1307
1318
1308
1319
def input_output_response (
1309
- sys ,T ,U = 0. ,X0 = 0 ,params = None ,
1320
+ sys ,T ,U = 0. ,X0 = 0 ,params = None ,ignore_errors = False ,
1310
1321
transpose = False ,return_x = False ,squeeze = None ,
1311
1322
solve_ivp_kwargs = None ,t_eval = 'T' ,** kwargs ):
1312
1323
"""Compute the output response of a system to a given input.
@@ -1382,6 +1393,11 @@ def input_output_response(
1382
1393
to 'RK45'.
1383
1394
solve_ivp_kwargs : dict, optional
1384
1395
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``.
1385
1401
1386
1402
Raises
1387
1403
------
@@ -1516,7 +1532,7 @@ def input_output_response(
1516
1532
X0 = np .hstack ([X0 ,np .zeros (sys .nstates - X0 .size )])
1517
1533
1518
1534
# Compute the number of states
1519
- nstates = _find_size (sys .nstates ,X0 )
1535
+ nstates = _find_size (sys .nstates ,X0 , "states" )
1520
1536
1521
1537
# create X0 if not given, test if X0 has correct shape
1522
1538
X0 = _check_convert_array (X0 , [(nstates ,), (nstates ,1 )],
@@ -1583,7 +1599,11 @@ def ivp_rhs(t, x):
1583
1599
ivp_rhs , (T0 ,Tf ),X0 ,t_eval = t_eval ,
1584
1600
vectorized = False ,** solve_ivp_kwargs )
1585
1601
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
1587
1607
1588
1608
# Compute inputs and outputs for each time point
1589
1609
u = np .zeros ((ninputs ,len (soln .t )))
@@ -1639,7 +1659,7 @@ def ivp_rhs(t, x):
1639
1659
u = np .transpose (np .array (u ))
1640
1660
1641
1661
# Mark solution as successful
1642
- soln .success = True # No way to fail
1662
+ soln .success , message = True , None # No way to fail
1643
1663
1644
1664
else :# Neither ctime or dtime??
1645
1665
raise TypeError ("Can't determine system type" )
@@ -1649,7 +1669,8 @@ def ivp_rhs(t, x):
1649
1669
output_labels = sys .output_labels ,input_labels = sys .input_labels ,
1650
1670
state_labels = sys .state_labels ,sysname = sys .name ,
1651
1671
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 )
1653
1674
1654
1675
1655
1676
def 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,
1732
1753
from scipy .optimize import root
1733
1754
1734
1755
# 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" )
1738
1759
1739
1760
# Convert x0, u0, y0 to arrays, if needed
1740
1761
if np .isscalar (x0 ):
@@ -1977,23 +1998,23 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw):
1977
1998
return sys .linearize (xeq ,ueq ,t = t ,params = params ,** kw )
1978
1999
1979
2000
1980
- def _find_size (sysval ,vecval ):
2001
+ def _find_size (sysval ,vecval , label ):
1981
2002
"""Utility function to find the size of a system parameter
1982
2003
1983
2004
If both parameters are not None, they must be consistent.
1984
2005
"""
1985
2006
if hasattr (vecval ,'__len__' ):
1986
2007
if sysval is not None and sysval != len (vecval ):
1987
- raise ValueError ("Inconsistent information to determine size "
1988
- " ofsystem component " )
2008
+ raise ValueError (
2009
+ f"inconsistent information for number of{ label } " )
1989
2010
return len (vecval )
1990
2011
# None or 0, which is a valid value for "a (sysval, ) vector of zeros".
1991
2012
if not vecval :
1992
2013
return 0 if sysval is None else sysval
1993
2014
elif sysval == 1 :
1994
2015
# (1, scalar) is also a valid combination from legacy code
1995
2016
return 1
1996
- raise ValueError ("can't determinesize ofsystem component " )
2017
+ raise ValueError (f "can't determinenumber of{ label } " )
1997
2018
1998
2019
1999
2020
# Function to create an interconnected system
@@ -2241,7 +2262,7 @@ def interconnect(
2241
2262
`outputs`, for more natural naming of SISO systems.
2242
2263
2243
2264
"""
2244
- from .statesp import StateSpace , LinearICSystem ,_convert_to_statespace
2265
+ from .statesp import LinearICSystem , StateSpace ,_convert_to_statespace
2245
2266
from .xferfcn import TransferFunction
2246
2267
2247
2268
dt = kwargs .pop ('dt' ,None )# bypass normal 'dt' processing
@@ -2551,7 +2572,7 @@ def interconnect(
2551
2572
return newsys
2552
2573
2553
2574
2554
- # Utility function to allow lists states, inputs
2575
+ # Utility function to allow listsof states, inputs
2555
2576
def _concatenate_list_elements (X ,name = 'X' ):
2556
2577
# If we were passed a list, concatenate the elements together
2557
2578
if isinstance (X , (tuple ,list )):
@@ -2574,13 +2595,14 @@ def _convert_static_iosystem(sys):
2574
2595
# Convert sys1 to an I/O system if needed
2575
2596
if isinstance (sys , (int ,float ,np .number )):
2576
2597
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 )
2578
2600
2579
2601
elif isinstance (sys ,np .ndarray ):
2580
2602
sys = np .atleast_2d (sys )
2581
2603
return NonlinearIOSystem (
2582
2604
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 )
2584
2606
2585
2607
def connection_table (sys ,show_names = False ,column_width = 32 ):
2586
2608
"""Print table of connections inside an interconnected system model.