4343# RMM, 2 April 2011: modified to work with new LTI structure (see ChangeLog)
4444# * Not tested: should still work on signal.ltisys objects
4545#
46+ # Sawyer B. Fuller (minster@uw.edu) 21 May 2020:
47+ # * added compatibility with discrete-time systems.
48+ #
4649# $Id$
4750
4851# Packages used by this module
5255import matplotlib .pyplot as plt
5356from numpy import array , poly1d , row_stack , zeros_like , real , imag
5457import scipy .signal # signal processing toolbox
58+ from .lti import isdtime
5559from .xferfcn import _convert_to_transfer_function
5660from .exception import ControlMIMONotImplemented
5761from .sisotool import _SisotoolUpdate
62+ from .grid import sgrid , zgrid
5863from . import config
5964
6065__all__ = ['root_locus' , 'rlocus' ]
@@ -131,6 +136,13 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None,
131136 # Convert numerator and denominator to polynomials if they aren't
132137 (nump , denp ) = _systopoly1d (sys )
133138
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+
134146 if kvect is None :
135147 start_mat = _RLFindRoots (nump , denp , [1 ])
136148 kvect , mymat , xlim , ylim = _default_gains (nump , denp , xlim , ylim )
@@ -163,10 +175,14 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None,
163175 [root .real for root in start_mat ],
164176 [root .imag for root in start_mat ],
165177 '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 )
166183 fig .suptitle (
167184 "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 ),
170186 fontsize = 12 if int (mpl .__version__ [0 ]) == 1 else 10 )
171187 fig .canvas .mpl_connect (
172188 'button_release_event' ,
@@ -199,20 +215,31 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None,
199215 ax .plot (real (col ), imag (col ), plotstr , label = 'rootlocus' )
200216
201217 # Set up plot axes and labels
202- if xlim :
203- ax .set_xlim (xlim )
204- if ylim :
205- ax .set_ylim (ylim )
206-
207218 ax .set_xlabel ('Real' )
208219 ax .set_ylabel ('Imaginary' )
220+
209221 if grid and sisotool :
210- _sgrid_func (f )
222+ if isdtime (sys , strict = True ):
223+ zgrid (ax = ax )
224+ else :
225+ _sgrid_func (f )
211226 elif grid :
212- _sgrid_func ()
227+ if isdtime (sys , strict = True ):
228+ zgrid (ax = ax )
229+ else :
230+ _sgrid_func ()
213231 else :
214232 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 )
216243
217244 return mymat , kvect
218245
@@ -567,12 +594,17 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False):
567594 if abs (K .real ) > 1e-8 and abs (K .imag / K .real ) < gain_tolerance and \
568595 event .inaxes == ax_rlocus .axes and K .real > 0. :
569596
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+
570602 # Display the parameters in the output window and figure
571603 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 ))
573605 fig .suptitle (
574606 "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 ),
576608 fontsize = 12 if int (mpl .__version__ [0 ]) == 1 else 10 )
577609
578610 # Remove the previous line
@@ -616,13 +648,13 @@ def _sgrid_func(fig=None, zeta=None, wn=None):
616648 if zeta is None :
617649 zeta = _default_zetas (xlim , ylim )
618650
619- angules = []
651+ angles = []
620652 for z in zeta :
621653 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 ))
623655 else :
624656 zeta .remove (z )
625- y_over_x = np .tan (angules )
657+ y_over_x = np .tan (angles )
626658
627659 # zeta-constant lines
628660
@@ -647,30 +679,30 @@ def _sgrid_func(fig=None, zeta=None, wn=None):
647679 ax .plot ([0 , 0 ], [ylim [0 ], ylim [1 ]],
648680 color = 'gray' , linestyle = 'dashed' , linewidth = 0.5 )
649681
650- angules = np .linspace (- 90 , 90 , 20 )* np .pi / 180
682+ angles = np .linspace (- 90 , 90 , 20 )* np .pi / 180
651683 if wn is None :
652684 wn = _default_wn (xlocator (), ylim )
653685
654686 for om in wn :
655687 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 )
658690 ax .plot (xp , yp , color = 'gray' ,
659691 linestyle = 'dashed' , linewidth = 0.5 )
660692 an = "%.2f" % - om
661693 ax .annotate (an , textcoords = 'data' , xy = [om , 0 ], fontsize = 8 )
662694
663695
664696def _default_zetas (xlim , ylim ):
665- """Return default list of dumps coefficients"""
697+ """Return default list of damping coefficients"""
666698 sep1 = - xlim [0 ]/ 4
667699 ang1 = [np .arctan ((sep1 * i )/ ylim [1 ]) for i in np .arange (1 , 4 , 1 )]
668700 sep2 = ylim [1 ] / 3
669701 ang2 = [np .arctan (- xlim [0 ]/ (ylim [1 ]- sep2 * i )) for i in np .arange (1 , 3 , 1 )]
670702
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 )
674706 return zeta .tolist ()
675707
676708
0 commit comments