Skip to content

Commit af8e70b

Browse files
committed
Merge branch 'master' into iosys
2 parents 9f5d669 + c99f72f commit af8e70b

40 files changed

+2199
-667
lines changed

control/bdalg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def feedback(sys1, sys2=1, sign=-1):
248248
elif isinstance(sys2, ss.StateSpace):
249249
sys1 = ss._convertToStateSpace(sys1)
250250
elif isinstance(sys2, frd.FRD):
251-
sys1 = ss._convertToFRD(sys1)
251+
sys1 = frd._convertToFRD(sys1, sys2.omega)
252252
else: # sys2 is a scalar.
253253
sys1 = tf._convert_to_transfer_function(sys1)
254254
sys2 = tf._convert_to_transfer_function(sys2)

control/canonical.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ def reachable_form(xsys):
8888
# Transformation from one form to another
8989
Tzx = solve(Wrx.T, Wrz.T).T # matrix right division, Tzx = Wrz * inv(Wrx)
9090

91-
if matrix_rank(Tzx) != xsys.states:
91+
# Check to make sure inversion was OK. Note that since we are inverting
92+
# Wrx and we already checked its rank, this exception should never occur
93+
if matrix_rank(Tzx) != xsys.states: # pragma: no cover
9294
raise ValueError("Transformation matrix singular to working precision.")
9395

9496
# Finally, compute the output matrix

control/config.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414
bode_number_of_samples = None # Bode plot number of samples
1515
bode_feature_periphery_decade = 1.0 # Bode plot feature periphery in decades
1616

17+
18+
def reset_defaults():
19+
"""Reset configuration values to their default values."""
20+
global bode_dB; bode_dB = False
21+
global bode_deg; bode_deg = True
22+
global bode_Hz; bode_Hz = False
23+
global bode_number_of_samples; bode_number_of_samples = None
24+
global bode_feature_periphery_decade; bode_feature_periphery_decade = 1.0
25+
26+
1727
# Set defaults to match MATLAB
1828
def use_matlab_defaults():
1929
"""
@@ -27,6 +37,7 @@ def use_matlab_defaults():
2737
global bode_deg; bode_deg = True
2838
global bode_Hz; bode_Hz = True
2939

40+
3041
# Set defaults to match FBS (Astrom and Murray)
3142
def use_fbs_defaults():
3243
"""
@@ -39,4 +50,5 @@ def use_fbs_defaults():
3950
# Bode plot defaults
4051
global bode_dB; bode_dB = False
4152
global bode_deg; bode_deg = True
42-
global bode_Hz; bode_Hz = True
53+
global bode_Hz; bode_Hz = False
54+

control/dtime.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ def c2d(sysc, Ts, method='zoh'):
112112
'''
113113
# Call the sample_system() function to do the work
114114
sysd = sample_system(sysc, Ts, method)
115+
116+
# TODO: is this check needed? If sysc is StateSpace, sysd is too?
115117
if isinstance(sysc, StateSpace) and not isinstance(sysd, StateSpace):
116-
return _convertToStateSpace(sysd)
118+
return _convertToStateSpace(sysd) # pragma: no cover
119+
117120
return sysd

control/exception.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,24 @@
3939
#
4040
# $Id$
4141

42-
class ControlSlycot(Exception):
42+
class ControlSlycot(ImportError):
4343
"""Exception for Slycot import. Used when we can't import a function
4444
from the slycot package"""
4545
pass
4646

47-
class ControlDimension(Exception):
47+
class ControlDimension(ValueError):
4848
"""Raised when dimensions of system objects are not correct"""
4949
pass
5050

51-
class ControlArgument(Exception):
51+
class ControlArgument(TypeError):
5252
"""Raised when arguments to a function are not correct"""
5353
pass
5454

55-
class ControlMIMONotImplemented(Exception):
55+
class ControlMIMONotImplemented(NotImplementedError):
5656
"""Function is not currently implemented for MIMO systems"""
5757
pass
5858

59-
class ControlNotImplemented(Exception):
59+
class ControlNotImplemented(NotImplementedError):
6060
"""Functionality is not yet implemented"""
6161
pass
6262

control/frdata.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"""
5050

5151
# External function declarations
52+
from warnings import warn
5253
import numpy as np
5354
from numpy import angle, array, empty, ones, \
5455
real, imag, matrix, absolute, eye, linalg, where, dot
@@ -187,9 +188,10 @@ def __add__(self, other):
187188

188189
if isinstance(other, FRD):
189190
# verify that the frequencies match
190-
if (other.omega != self.omega).any():
191-
print("Warning: frequency points do not match; expect"
192-
" truncation and interpolation")
191+
if len(other.omega) != len(self.omega) or \
192+
(other.omega != self.omega).any():
193+
warn("Frequency points do not match; expect"
194+
" truncation and interpolation.")
193195

194196
# Convert the second argument to a frequency response function.
195197
# or re-base the frd to the current omega (if needed)
@@ -340,8 +342,9 @@ def evalfr(self, omega):
340342
intermediate values.
341343
342344
"""
343-
warn("FRD.evalfr(omega) will be deprecated in a future release of python-control; use sys.eval(omega) instead",
344-
PendingDeprecationWarning)
345+
warn("FRD.evalfr(omega) will be deprecated in a future release "
346+
"of python-control; use sys.eval(omega) instead",
347+
PendingDeprecationWarning) # pragma: no coverage
345348
return self._evalfr(omega)
346349

347350
# Define the `eval` function to evaluate an FRD at a given (real)
@@ -352,7 +355,7 @@ def evalfr(self, omega):
352355
def eval(self, omega):
353356
"""Evaluate a transfer function at a single angular frequency.
354357
355-
self._evalfr(omega) returns the value of the frequency response
358+
self.evalfr(omega) returns the value of the frequency response
356359
at frequency omega.
357360
358361
Note that a "normal" FRD only returns values for which there is an
@@ -462,7 +465,8 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
462465

463466
if isinstance(sys, FRD):
464467
omega.sort()
465-
if (abs(omega - sys.omega) < FRD.epsw).all():
468+
if len(omega) == len(sys.omega) and \
469+
(abs(omega - sys.omega) < FRD.epsw).all():
466470
# frequencies match, and system was already frd; simply use
467471
return sys
468472

control/freqplot.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -603,18 +603,24 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None,
603603
----------
604604
syslist : list of LTI
605605
List of linear input/output systems (single system is OK)
606-
Hz: boolean
606+
607+
Hz : bool
607608
If True, the limits (first and last value) of the frequencies
608609
are set to full decades in Hz so it fits plotting with logarithmic
609610
scale in Hz otherwise in rad/s. Omega is always returned in rad/sec.
610-
number_of_samples: int
611-
Number of samples to generate
612-
feature_periphery_decade: float
611+
612+
number_of_samples : int, optional
613+
Number of samples to generate. Defaults to ``numpy.logspace`` default
614+
value.
615+
616+
feature_periphery_decade : float, optional
613617
Defines how many decades shall be included in the frequency range on
614618
both sides of features (poles, zeros).
615-
Example: If there is a feature, e.g. a pole, at 1Hz and feature_periphery_decade=1.
616-
then the range of frequencies shall span 0.1 .. 10 Hz.
617-
The default value is read from config.bode_feature_periphery_decade.
619+
620+
Example: If there is a feature, e.g. a pole, at 1 Hz and
621+
feature_periphery_decade=1., then the range of frequencies shall span
622+
0.1 .. 10 Hz. The default value is read from
623+
``config.bode_feature_periphery_decade``.
618624
619625
Returns
620626
-------
@@ -626,6 +632,7 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None,
626632
>>> from matlab import ss
627633
>>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.")
628634
>>> omega = default_frequency_range(sys)
635+
629636
"""
630637
# This code looks at the poles and zeros of all of the systems that
631638
# we are plotting and sets the frequency range to be one decade above

control/matlab/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
from ..margins import margin
8282
from ..rlocus import rlocus
8383
from ..dtime import c2d
84+
from ..sisotool import sisotool
8485

8586
# Import functions specific to Matlab compatibility package
8687
from .timeresp import *
@@ -241,6 +242,7 @@
241242
242243
== ========================== ============================================
243244
\* :func:`rlocus` evans root locus
245+
\* :func:`sisotool` SISO controller design
244246
\* :func:`~control.place` pole placement
245247
\ estim form estimator given estimator gain
246248
\ reg form regulator given state-feedback and

control/rlocus.py

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='b' if int(matplot
138138
f.canvas.mpl_connect(
139139
'button_release_event',partial(_RLClickDispatcher,sys=sys, fig=f,ax_rlocus=f.axes[1],plotstr=plotstr, sisotool=sisotool, bode_plot_params=kwargs['bode_plot_params'],tvect=kwargs['tvect']))
140140

141+
# zoom update on xlim/ylim changed, only then data on new limits
142+
# is available, i.e., cannot combine with _RLClickDispatcher
143+
dpfun = partial(
144+
_RLZoomDispatcher, sys=sys, ax_rlocus=ax, plotstr=plotstr)
145+
ax.callbacks.connect('xlim_changed', dpfun)
146+
ax.callbacks.connect('ylim_changed', dpfun)
147+
141148
# plot open loop poles
142149
poles = array(denp.r)
143150
ax.plot(real(poles), imag(poles), 'x')
@@ -156,6 +163,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='b' if int(matplot
156163
ax.set_xlim(xlim)
157164
if ylim:
158165
ax.set_ylim(ylim)
166+
159167
ax.set_xlabel('Real')
160168
ax.set_ylabel('Imaginary')
161169
if grid and sisotool:
@@ -263,7 +271,7 @@ def _indexes_filt(mymat,tolerance,zoom_xlim=None,zoom_ylim=None):
263271
break
264272

265273
# Check if the zoom box is not overshot and insert points where neccessary
266-
if len(indexes_too_far_filtered) == 0 and len(mymat) <300:
274+
if len(indexes_too_far_filtered) == 0 and len(mymat) <500:
267275
limits = [zoom_xlim[0],zoom_xlim[1],zoom_ylim[0],zoom_ylim[1]]
268276
for index,limit in enumerate(limits):
269277
if index <= 1:
@@ -411,23 +419,30 @@ def _RLSortRoots(mymat):
411419
prevrow = sorted[n, :]
412420
return sorted
413421

414-
def _RLClickDispatcher(event,sys,fig,ax_rlocus,plotstr,sisotool=False,bode_plot_params=None,tvect=None):
415-
"""Rootlocus plot click dispatcher"""
422+
def _RLZoomDispatcher(event, sys, ax_rlocus, plotstr):
423+
"""Rootlocus plot zoom dispatcher"""
416424

417-
# If zoom is used on the rootlocus plot smooth and update it
418-
if plt.get_current_fig_manager().toolbar.mode in ['zoom rect','pan/zoom'] and event.inaxes == ax_rlocus.axes:
419-
(nump, denp) = _systopoly1d(sys)
420-
xlim,ylim = ax_rlocus.get_xlim(),ax_rlocus.get_ylim()
425+
nump, denp = _systopoly1d(sys)
426+
xlim, ylim = ax_rlocus.get_xlim(), ax_rlocus.get_ylim()
427+
428+
kvect, mymat, xlim, ylim = _default_gains(
429+
nump, denp, xlim=None, ylim=None, zoom_xlim=xlim, zoom_ylim=ylim)
430+
_removeLine('rootlocus', ax_rlocus)
431+
432+
for i, col in enumerate(mymat.T):
433+
ax_rlocus.plot(real(col), imag(col), plotstr, label='rootlocus',
434+
scalex=False, scaley=False)
421435

422-
kvect,mymat, xlim,ylim = _default_gains(nump, denp,xlim=None,ylim=None, zoom_xlim=xlim,zoom_ylim=ylim)
423-
_removeLine('rootlocus', ax_rlocus)
436+
def _RLClickDispatcher(event,sys,fig,ax_rlocus,plotstr,sisotool=False,bode_plot_params=None,tvect=None):
437+
"""Rootlocus plot click dispatcher"""
424438

425-
for i,col in enumerate(mymat.T):
426-
ax_rlocus.plot(real(col), imag(col), plotstr,label='rootlocus')
439+
# Zoom is handled by specialized callback above, only do gain plot
440+
if event.inaxes == ax_rlocus.axes and \
441+
plt.get_current_fig_manager().toolbar.mode not in \
442+
{'zoom rect','pan/zoom'}:
427443

428-
# if a point is clicked on the rootlocus plot visually emphasize it
429-
else:
430-
K = _RLFeedbackClicksPoint(event, sys, fig,ax_rlocus,sisotool)
444+
# if a point is clicked on the rootlocus plot visually emphasize it
445+
K = _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool)
431446
if sisotool and K is not None:
432447
_SisotoolUpdate(sys, fig, K, bode_plot_params, tvect)
433448

@@ -440,21 +455,27 @@ def _RLFeedbackClicksPoint(event,sys,fig,ax_rlocus,sisotool=False):
440455

441456
(nump, denp) = _systopoly1d(sys)
442457

458+
xlim = ax_rlocus.get_xlim()
459+
ylim = ax_rlocus.get_ylim()
460+
x_tolerance = 0.05 * abs((xlim[1] - xlim[0]))
461+
y_tolerance = 0.05 * abs((ylim[1] - ylim[0]))
462+
gain_tolerance = np.mean([x_tolerance, y_tolerance])*0.1
463+
443464
# Catch type error when event click is in the figure but not in an axis
444465
try:
445466
s = complex(event.xdata, event.ydata)
446467
K = -1. / sys.horner(s)
468+
K_xlim = -1. / sys.horner(complex(event.xdata + 0.05 * abs(xlim[1] - xlim[0]), event.ydata))
469+
K_ylim = -1. / sys.horner(complex(event.xdata, event.ydata + 0.05 * abs(ylim[1] - ylim[0])))
447470

448471
except TypeError:
449472
K = float('inf')
473+
K_xlim = float('inf')
474+
K_ylim = float('inf')
450475

451-
xlim = ax_rlocus.get_xlim()
452-
ylim = ax_rlocus.get_ylim()
453-
x_tolerance = 0.05 * (xlim[1] - xlim[0])
454-
y_tolerance = 0.05 * (ylim[1] - ylim[0])
455-
gain_tolerance = np.min([x_tolerance, y_tolerance])*1e-1
476+
gain_tolerance += 0.1*max([abs(K_ylim.imag/K_ylim.real),abs(K_xlim.imag/K_xlim.real)])
456477

457-
if abs(K.real) > 1e-8 and abs(K.imag / K.real) < gain_tolerance and event.inaxes == ax_rlocus.axes:
478+
if abs(K.real) > 1e-8 and abs(K.imag / K.real) < gain_tolerance and event.inaxes == ax_rlocus.axes and K.real > 0.:
458479

459480
# Display the parameters in the output window and figure
460481
print("Clicked at %10.4g%+10.4gj gain %10.4g damp %10.4g" %

control/sisotool.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ def _SisotoolUpdate(sys,fig,K,bode_plot_params,tvect=None):
131131
ax_rlocus.get_xaxis().set_label_coords(0.5, -0.15)
132132
ax_rlocus.get_yaxis().set_label_coords(-0.15, 0.5)
133133

134+
135+
134136
# Generate the step response and plot it
135137
sys_closed = (K*sys).feedback(1)
136138
if tvect is None:

0 commit comments

Comments
 (0)