Skip to content

Commit 0f03778

Browse files
Merge branch 'python-control:main' into main
2 parents 0f08b1e + ad6b49e commit 0f03778

19 files changed

+930
-137
lines changed

control/bdalg.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,11 @@ def negate(sys):
201201
--------
202202
>>> G = ct.tf([2], [1, 1])
203203
>>> G.dcgain()
204-
2.0
204+
np.float64(2.0)
205205
206206
>>> Gn = ct.negate(G) # Same as sys2 = -sys1.
207207
>>> Gn.dcgain()
208-
-2.0
208+
np.float64(-2.0)
209209
210210
"""
211211
return -sys

control/descfcn.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -525,11 +525,11 @@ class saturation_nonlinearity(DescribingFunctionNonlinearity):
525525
--------
526526
>>> nl = ct.saturation_nonlinearity(5)
527527
>>> nl(1)
528-
1
528+
np.int64(1)
529529
>>> nl(10)
530-
5
530+
np.int64(5)
531531
>>> nl(-10)
532-
-5
532+
np.int64(-5)
533533
534534
"""
535535
def __init__(self, ub=1, lb=None):

control/freqplot.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,7 +1919,7 @@ def _parse_linestyle(style_name, allow_false=False):
19191919
# Internal function to add arrows to a curve
19201920
def _add_arrows_to_line2D(
19211921
axes, line, arrow_locs=[0.2, 0.4, 0.6, 0.8],
1922-
arrowstyle='-|>', arrowsize=1, dir=1, transform=None):
1922+
arrowstyle='-|>', arrowsize=1, dir=1):
19231923
"""
19241924
Add arrows to a matplotlib.lines.Line2D at selected locations.
19251925
@@ -1930,7 +1930,6 @@ def _add_arrows_to_line2D(
19301930
arrow_locs: list of locations where to insert arrows, % of total length
19311931
arrowstyle: style of the arrow
19321932
arrowsize: size of the arrow
1933-
transform: a matplotlib transform instance, default to data coordinates
19341933
19351934
Returns:
19361935
--------
@@ -1939,13 +1938,13 @@ def _add_arrows_to_line2D(
19391938
Based on https://stackoverflow.com/questions/26911898/
19401939
19411940
"""
1941+
# Get the coordinates of the line, in plot coordinates
19421942
if not isinstance(line, mpl.lines.Line2D):
19431943
raise ValueError("expected a matplotlib.lines.Line2D object")
19441944
x, y = line.get_xdata(), line.get_ydata()
19451945

1946-
arrow_kw = {
1947-
"arrowstyle": arrowstyle,
1948-
}
1946+
# Determine the arrow properties
1947+
arrow_kw = {"arrowstyle": arrowstyle}
19491948

19501949
color = line.get_color()
19511950
use_multicolor_lines = isinstance(color, np.ndarray)
@@ -1960,36 +1959,43 @@ def _add_arrows_to_line2D(
19601959
else:
19611960
arrow_kw['linewidth'] = linewidth
19621961

1963-
if transform is None:
1964-
transform = axes.transData
1962+
# Figure out the size of the axes (length of diagonal)
1963+
xlim, ylim = axes.get_xlim(), axes.get_ylim()
1964+
ul, lr = np.array([xlim[0], ylim[0]]), np.array([xlim[1], ylim[1]])
1965+
diag = np.linalg.norm(ul - lr)
19651966

19661967
# Compute the arc length along the curve
19671968
s = np.cumsum(np.sqrt(np.diff(x) ** 2 + np.diff(y) ** 2))
19681969

1970+
# Truncate the number of arrows if the curve is short
1971+
# TODO: figure out a smarter way to do this
1972+
frac = min(s[-1] / diag, 1)
1973+
if len(arrow_locs) and frac < 0.05:
1974+
arrow_locs = [] # too short; no arrows at all
1975+
elif len(arrow_locs) and frac < 0.2:
1976+
arrow_locs = [0.5] # single arrow in the middle
1977+
1978+
# Plot the arrows (and return list if patches)
19691979
arrows = []
19701980
for loc in arrow_locs:
19711981
n = np.searchsorted(s, s[-1] * loc)
19721982

1973-
# Figure out what direction to paint the arrow
1974-
if dir == 1:
1975-
arrow_tail = (x[n], y[n])
1976-
arrow_head = (np.mean(x[n:n + 2]), np.mean(y[n:n + 2]))
1977-
elif dir == -1:
1978-
# Orient the arrow in the other direction on the segment
1979-
arrow_tail = (x[n + 1], y[n + 1])
1980-
arrow_head = (np.mean(x[n:n + 2]), np.mean(y[n:n + 2]))
1981-
else:
1982-
raise ValueError("unknown value for keyword 'dir'")
1983+
if dir == 1 and n == 0:
1984+
# Move the arrow forward by one if it is at start of a segment
1985+
n = 1
1986+
1987+
# Place the head of the arrow at the desired location
1988+
arrow_head = [x[n], y[n]]
1989+
arrow_tail = [x[n - dir], y[n - dir]]
19831990

19841991
p = mpl.patches.FancyArrowPatch(
1985-
arrow_tail, arrow_head, transform=transform, lw=0,
1992+
arrow_tail, arrow_head, transform=axes.transData, lw=0,
19861993
**arrow_kw)
19871994
axes.add_patch(p)
19881995
arrows.append(p)
19891996
return arrows
19901997

19911998

1992-
19931999
#
19942000
# Function to compute Nyquist curve offsets
19952001
#

control/lti.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ def dcgain(sys):
525525
--------
526526
>>> G = ct.tf([1], [1, 2])
527527
>>> ct.dcgain(G) # doctest: +SKIP
528-
0.5
528+
np.float(0.5)
529529
530530
"""
531531
return sys.dcgain()

control/modelsimp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def hsvd(sys):
8787
>>> G = ct.tf2ss([1], [1, 2])
8888
>>> H = ct.hsvd(G)
8989
>>> H[0]
90-
0.25
90+
np.float64(0.25)
9191
9292
"""
9393
# TODO: implement for discrete time systems

control/nlsys.py

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@
1818
1919
"""
2020

21-
import numpy as np
22-
import scipy as sp
2321
import copy
2422
from warnings import warn
2523

24+
import numpy as np
25+
import scipy as sp
26+
2627
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
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.
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

13081319
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,
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

16551676
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,
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
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
25552576
def _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

25852607
def connection_table(sys, show_names=False, column_width=32):
25862608
"""Print table of connections inside an interconnected system model.

0 commit comments

Comments
 (0)