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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy