43
43
# RMM, 2 April 2011: modified to work with new LTI structure (see ChangeLog)
44
44
# * Not tested: should still work on signal.ltisys objects
45
45
#
46
+ # Sawyer B. Fuller (minster@uw.edu) 21 May 2020:
47
+ # * added compatibility with discrete-time systems.
48
+ #
46
49
# $Id$
47
50
48
51
# Packages used by this module
52
55
import matplotlib .pyplot as plt
53
56
from numpy import array , poly1d , row_stack , zeros_like , real , imag
54
57
import scipy .signal # signal processing toolbox
58
+ from .lti import isdtime
55
59
from .xferfcn import _convert_to_transfer_function
56
60
from .exception import ControlMIMONotImplemented
57
61
from .sisotool import _SisotoolUpdate
62
+ from .grid import sgrid , zgrid
58
63
from . import config
59
64
60
65
__all__ = ['root_locus' , 'rlocus' ]
@@ -131,6 +136,13 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None,
131
136
# Convert numerator and denominator to polynomials if they aren't
132
137
(nump , denp ) = _systopoly1d (sys )
133
138
139
+ # if discrete-time system and if xlim and ylim are not given,
140
+ # that we a view of the unit circle
141
+ if xlim is None and isdtime (sys , strict = True ):
142
+ xlim = (- 1.2 , 1.2 )
143
+ if ylim is None and isdtime (sys , strict = True ):
144
+ xlim = (- 1.3 , 1.3 )
145
+
134
146
if kvect is None :
135
147
start_mat = _RLFindRoots (nump , denp , [1 ])
136
148
kvect , mymat , xlim , ylim = _default_gains (nump , denp , xlim , ylim )
@@ -163,10 +175,14 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None,
163
175
[root .real for root in start_mat ],
164
176
[root .imag for root in start_mat ],
165
177
'm.' , marker = 's' , markersize = 8 , zorder = 20 , label = 'gain_point' )
178
+ s = start_mat [0 ][0 ]
179
+ if isdtime (sys , strict = True ):
180
+ zeta = - np .cos (np .angle (np .log (s )))
181
+ else :
182
+ zeta = - 1 * s .real / abs (s )
166
183
fig .suptitle (
167
184
"Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" %
168
- (start_mat [0 ][0 ].real , start_mat [0 ][0 ].imag ,
169
- 1 , - 1 * start_mat [0 ][0 ].real / abs (start_mat [0 ][0 ])),
185
+ (s .real , s .imag , 1 , zeta ),
170
186
fontsize = 12 if int (mpl .__version__ [0 ]) == 1 else 10 )
171
187
fig .canvas .mpl_connect (
172
188
'button_release_event' ,
@@ -199,20 +215,31 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None,
199
215
ax .plot (real (col ), imag (col ), plotstr , label = 'rootlocus' )
200
216
201
217
# Set up plot axes and labels
202
- if xlim :
203
- ax .set_xlim (xlim )
204
- if ylim :
205
- ax .set_ylim (ylim )
206
-
207
218
ax .set_xlabel ('Real' )
208
219
ax .set_ylabel ('Imaginary' )
220
+
209
221
if grid and sisotool :
210
- _sgrid_func (f )
222
+ if isdtime (sys , strict = True ):
223
+ zgrid (ax = ax )
224
+ else :
225
+ _sgrid_func (f )
211
226
elif grid :
212
- _sgrid_func ()
227
+ if isdtime (sys , strict = True ):
228
+ zgrid (ax = ax )
229
+ else :
230
+ _sgrid_func ()
213
231
else :
214
232
ax .axhline (0. , linestyle = ':' , color = 'k' , zorder = - 20 )
215
- ax .axvline (0. , linestyle = ':' , color = 'k' )
233
+ ax .axvline (0. , linestyle = ':' , color = 'k' , zorder = - 20 )
234
+ if isdtime (sys , strict = True ):
235
+ ax .add_patch (plt .Circle ((0 ,0 ), radius = 1.0 ,
236
+ linestyle = ':' , edgecolor = 'k' , linewidth = 1.5 ,
237
+ fill = False , zorder = - 20 ))
238
+
239
+ if xlim :
240
+ ax .set_xlim (xlim )
241
+ if ylim :
242
+ ax .set_ylim (ylim )
216
243
217
244
return mymat , kvect
218
245
@@ -567,12 +594,17 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False):
567
594
if abs (K .real ) > 1e-8 and abs (K .imag / K .real ) < gain_tolerance and \
568
595
event .inaxes == ax_rlocus .axes and K .real > 0. :
569
596
597
+ if isdtime (sys , strict = True ):
598
+ zeta = - np .cos (np .angle (np .log (s )))
599
+ else :
600
+ zeta = - 1 * s .real / abs (s )
601
+
570
602
# Display the parameters in the output window and figure
571
603
print ("Clicked at %10.4g%+10.4gj gain %10.4g damp %10.4g" %
572
- (s .real , s .imag , K .real , - 1 * s . real / abs ( s ) ))
604
+ (s .real , s .imag , K .real , zeta ))
573
605
fig .suptitle (
574
606
"Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" %
575
- (s .real , s .imag , K .real , - 1 * s . real / abs ( s ) ),
607
+ (s .real , s .imag , K .real , zeta ),
576
608
fontsize = 12 if int (mpl .__version__ [0 ]) == 1 else 10 )
577
609
578
610
# Remove the previous line
@@ -616,13 +648,13 @@ def _sgrid_func(fig=None, zeta=None, wn=None):
616
648
if zeta is None :
617
649
zeta = _default_zetas (xlim , ylim )
618
650
619
- angules = []
651
+ angles = []
620
652
for z in zeta :
621
653
if (z >= 1e-4 ) and (z <= 1 ):
622
- angules .append (np .pi / 2 + np .arcsin (z ))
654
+ angles .append (np .pi / 2 + np .arcsin (z ))
623
655
else :
624
656
zeta .remove (z )
625
- y_over_x = np .tan (angules )
657
+ y_over_x = np .tan (angles )
626
658
627
659
# zeta-constant lines
628
660
@@ -647,30 +679,30 @@ def _sgrid_func(fig=None, zeta=None, wn=None):
647
679
ax .plot ([0 , 0 ], [ylim [0 ], ylim [1 ]],
648
680
color = 'gray' , linestyle = 'dashed' , linewidth = 0.5 )
649
681
650
- angules = np .linspace (- 90 , 90 , 20 )* np .pi / 180
682
+ angles = np .linspace (- 90 , 90 , 20 )* np .pi / 180
651
683
if wn is None :
652
684
wn = _default_wn (xlocator (), ylim )
653
685
654
686
for om in wn :
655
687
if om < 0 :
656
- yp = np .sin (angules )* np .abs (om )
657
- xp = - np .cos (angules )* np .abs (om )
688
+ yp = np .sin (angles )* np .abs (om )
689
+ xp = - np .cos (angles )* np .abs (om )
658
690
ax .plot (xp , yp , color = 'gray' ,
659
691
linestyle = 'dashed' , linewidth = 0.5 )
660
692
an = "%.2f" % - om
661
693
ax .annotate (an , textcoords = 'data' , xy = [om , 0 ], fontsize = 8 )
662
694
663
695
664
696
def _default_zetas (xlim , ylim ):
665
- """Return default list of dumps coefficients"""
697
+ """Return default list of damping coefficients"""
666
698
sep1 = - xlim [0 ]/ 4
667
699
ang1 = [np .arctan
F438
span>((sep1 * i )/ ylim [1 ]) for i in np .arange (1 , 4 , 1 )]
668
700
sep2 = ylim [1 ] / 3
669
701
ang2 = [np .arctan (- xlim [0 ]/ (ylim [1 ]- sep2 * i )) for i in np .arange (1 , 3 , 1 )]
670
702
671
- angules = np .concatenate ((ang1 , ang2 ))
672
- angules = np .insert (angules , len (angules ), np .pi / 2 )
673
- zeta = np .sin (angules )
703
+ angles = np .concatenate ((ang1 , ang2 ))
704
+ angles = np .insert (angles , len (angles ), np .pi / 2 )
705
+ zeta = np .sin (angles )
674
706
return zeta .tolist ()
675
707
676
708
0 commit comments