44# Date: 7 Sep 2009
55#
66# This file contains functions that compute poles, zeros and related
7- # quantities for a linear system.
7+ # quantities for a linear system, as well as the main functions for
8+ # storing and plotting pole/zero and root locus diagrams. (The actual
9+ # computation of root locus diagrams is in rlocus.py.)
810#
911
1012import numpy as np
@@ -41,14 +43,11 @@ def plot(self, *args, **kwargs):
4143
4244class RootLocusData :
4345 def __init__ (
44- self , poles , zeros , gains = None , loci = None , xlim = None , ylim = None ,
45- dt = None , sysname = None ):
46+ self , poles , zeros , gains = None , loci = None , dt = None , sysname = None ):
4647 self .poles = poles
4748 self .zeros = zeros
4849 self .gains = gains
4950 self .loci = loci
50- self .xlim = xlim
51- self .ylim = ylim
5251 self .dt = dt
5352 self .sysname = sysname
5453
@@ -78,7 +77,8 @@ def pole_zero_map(sysdata):
7877
7978
8079# Root locus map
81- def root_locus_map (sysdata , gains = None , xlim = None , ylim = None ):
80+ # TODO: use rlocus.py computation instead
81+ def root_locus_map (sysdata , gains = None ):
8282 # Convert the first argument to a list
8383 syslist = sysdata if isinstance (sysdata , (list , tuple )) else [sysdata ]
8484
@@ -94,22 +94,16 @@ def root_locus_map(sysdata, gains=None, xlim=None, ylim=None):
9494 # Convert numerator and denominator to polynomials if they aren't
9595 nump , denp = _systopoly1d (sys [0 , 0 ])
9696
97- if xlim is None and sys .isdtime (strict = True ):
98- xlim = (- 1.2 , 1.2 )
99- if ylim is None and sys .isdtime (strict = True ):
100- xlim = (- 1.3 , 1.3 )
101-
10297 if gains is None :
103- kvect , root_array , xlim , ylim = _default_gains (
104- nump , denp , xlim , ylim )
98+ kvect , root_array , _ , _ = _default_gains (nump , denp , None , None )
10599 else :
106100 kvect = np .atleast_1d (gains )
107101 root_array = _RLFindRoots (nump , denp , kvect )
108102 root_array = _RLSortRoots (root_array )
109103
110104 responses .append (RootLocusData (
111105 sys .poles (), sys .zeros (), kvect , root_array ,
112- dt = sys .dt , sysname = sys .name , xlim = xlim , ylim = ylim ))
106+ dt = sys .dt , sysname = sys .name ))
113107
114108 if isinstance (sysdata , (list , tuple )):
115109 return RootLocusList (responses )
@@ -203,7 +197,7 @@ def pole_zero_plot(
203197 return poles , zeros
204198
205199 # Initialize the figure
206- # TODO: turn into standard utility function
200+ # TODO: turn into standard utility function (from plotutil.py?)
207201 fig = plt .gcf ()
208202 axs = fig .get_axes ()
209203 if len (axs ) > 1 :
@@ -213,21 +207,22 @@ def pole_zero_plot(
213207 with plt .rc_context (freqplot_rcParams ):
214208 if grid :
215209 plt .clf ()
216- if all ([response .dt in [ 0 , None ] for response in data ]):
210+ if all ([isctime ( dt = response .dt ) for response in data ]):
217211 ax , fig = sgrid ()
218- elif all ([response .dt > 0 for response in data ]):
212+ elif all ([isdtime ( dt = response .dt ) for response in data ]):
219213 ax , fig = zgrid ()
220214 else :
221215 ValueError (
222216 "incompatible time responses; don't know how to grid" )
223217 elif len (axs ) == 0 :
224- ax , fig = nogrid ()
218+ ax , fig = nogrid (data [ 0 ]. dt ) # use first response timebase
225219 else :
226- # Use the existing axes
220+ # Use the existing axes and any grid that is there
221+ # TODO: allow axis to be overriden via parameter
227222 ax = axs [0 ]
228223
229- # Handle color cycle manually as all singular values
230- # of the same systems are expected to be of the same color
224+ # Handle color cycle manually as all root locus segments
225+ # of the same system are expected to be of the same color
231226 color_cycle = plt .rcParams ['axes.prop_cycle' ].by_key ()['color' ]
232227 color_offset = 0
233228 if len (ax .lines ) > 0 :
@@ -240,6 +235,7 @@ def pole_zero_plot(
240235 for i , j in itertools .product (range (out .shape [0 ]), range (out .shape [1 ])):
241236 out [i , j ] = [] # unique list in each element
242237
238+ # Plot the responses (and keep track of axes limits)
243239 xlim , ylim = ax .get_xlim (), ax .get_ylim ()
244240 for idx , response in enumerate (pzmap_responses ):
245241 poles = response .poles
@@ -272,13 +268,14 @@ def pole_zero_plot(
272268 real (locus ), imag (locus ), color = color ,
273269 label = response .sysname )
274270
275- # Compute the axis limits to use
276- if response .xlim is not None :
277- xlim = (min (xlim [0 ], response .xlim [0 ]),
278- max (xlim [1 ], response .xlim [1 ]))
279- if response .ylim is not None :
280- ylim = (min (ylim [0 ], response .ylim [0 ]),
281- max (ylim [1 ], response .ylim [1 ]))
271+ # Compute the axis limits to use based on the response
272+ resp_xlim , resp_ylim = _compute_root_locus_limits (response .loci )
273+
274+ # Keep track of the current limits
275+ xlim = [min (xlim [0 ], resp_xlim [0 ]), max (xlim [1 ], resp_xlim [1 ])]
276+ ylim = [min (ylim [0 ], resp_ylim [0 ]), max (ylim [1 ], resp_ylim [1 ])]
277+
278+ # TODO: add arrows to root loci (reuse Nyquist arrow code?)
282279
283280 # Set up the limits for the plot
284281 ax .set_xlim (xlim if xlim_user is None else xlim_user )
@@ -329,4 +326,31 @@ def pole_zero_plot(
329326 return out
330327
331328
329+ # Utility function to compute limits for root loci
330+ def _compute_root_locus_limits (loci ):
331+ # Go through each locus
332+ xlim , ylim = [0 , 0 ], 0
333+ for locus in loci .transpose ():
334+ # Include all starting points
335+ xlim = [min (xlim [0 ], locus [0 ].real ), max (xlim [1 ], locus [0 ].real )]
336+ ylim = max (ylim , locus [0 ].imag )
337+
338+ # Find the local maxima of root locus curve
339+ xpeaks = np .where (
340+ np .diff (np .abs (locus .real )) < 0 , locus .real [0 :- 1 ], 0 )
341+ xlim = [min (xlim [0 ], np .min (xpeaks )), max (xlim [1 ], np .max (xpeaks ))]
342+
343+ ypeaks = np .where (
344+ np .diff (np .abs (locus .imag )) < 0 , locus .imag [0 :- 1 ], 0 )
345+ ylim = max (ylim , np .max (ypeaks ))
346+
347+ # Adjust the limits to include some space around features
348+ rho = 1.5
349+ xlim [0 ] = rho * xlim [0 ] if xlim [0 ] < 0 else 0
350+ xlim [1 ] = rho * xlim [1 ] if xlim [1 ] > 0 else 0
351+ ylim = rho * ylim if ylim > 0 else np .max (np .abs (xlim ))
352+
353+ return xlim , [- ylim , ylim ]
354+
355+
332356pzmap = pole_zero_plot
0 commit comments