88# storing and plotting pole/zero and root locus diagrams. (The actual
99# computation of root locus diagrams is in rlocus.py.)
1010#
11- # TODO (Sep 2023):
12- # * Test out ability to set line styles
13- # - Make compatible with other plotting (and refactor?)
14- # - Allow line fmt to be overwritten (including color=CN for different
15- # colors for each segment?)
16- # * Add ability to set style of root locus click point
17- # - Sort out where default parameter values should live (pzmap vs rlocus)
18- # * Decide whether click functionality should be in rlocus.py
19- # * Add back print_gain option to sisotool (and any other options)
20- #
2111
2212import numpy as np
2313from numpy import real , imag , linspace , exp , cos , sin , sqrt
3424from .freqplot import _freqplot_defaults , _get_line_labels
3525from . import config
3626
37- __all__ = ['pole_zero_map' , 'pole_zero_plot' , 'pzmap' ]
27+ __all__ = ['pole_zero_map' , 'pole_zero_plot' , 'pzmap' , 'PoleZeroData' ]
3828
3929
4030# Define default parameter values for this module
5040# Classes for keeping track of pzmap plots
5141#
5242# The PoleZeroData class keeps track of the information that is on a
53- # pole- zero plot.
43+ # pole/ zero plot.
5444#
5545# In addition to the locations of poles and zeros, you can also save a set
5646# of gains and loci for use in generating a root locus plot. The gain
5747# variable is a 1D array consisting of a list of increasing gains. The
5848# loci variable is a 2D array indexed by [gain_idx, root_idx] that can be
5949# plotted using the `pole_zero_plot` function.
6050#
61- # The PoleZeroList class is used to return a list of pole- zero plots. It
51+ # The PoleZeroList class is used to return a list of pole/ zero plots. It
6252# is a lightweight wrapper on the built-in list class that includes a
6353# `plot` method, allowing plotting a set of root locus diagrams.
6454#
6555class PoleZeroData :
56+ """Pole/zero data object.
57+
58+ This class is used as the return type for computing pole/zero responses
59+ and root locus diagrams. It contains information on the location of
60+ system poles and zeros, as well as the gains and loci for root locus
61+ diagrams.
62+
63+ Attributes
64+ ----------
65+ poles : ndarray
66+ 1D array of system poles.
67+ zeros : ndarray
68+ 1D array of system zeros.
69+ gains : ndarray, optional
70+ 1D array of gains for root locus plots.
71+ loci : ndarray, optiona
72+ 2D array of poles, with each row corresponding to a gain.
73+ sysname : str, optional
74+ System name.
75+ sys : StateSpace or TransferFunction
76+ System corresponding to the data.
77+
78+ """
6679 def __init__ (
6780 self , poles , zeros , gains = None , loci = None , dt = None , sysname = None ,
6881 sys = None ):
82+ """Create a pole/zero map object.
83+
84+ Parameters
85+ ----------
86+ poles : ndarray
87+ 1D array of system poles.
88+ zeros : ndarray
89+ 1D array of system zeros.
90+ gains : ndarray, optional
91+ 1D array of gains for root locus plots.
92+ loci : ndarray, optiona
93+ 2D array of poles, with each row corresponding to a gain.
94+ sysname : str, optional
95+ System name.
96+ sys : StateSpace or TransferFunction
97+ System corresponding to the data.
98+
99+ """
69100 self .poles = poles
70101 self .zeros = zeros
71102 self .gains = gains
@@ -79,17 +110,51 @@ def __iter__(self):
79110 return iter ((self .poles , self .zeros ))
80111
81112 def plot (self , * args , ** kwargs ):
113+ """Plot the pole/zero data.
114+
115+ See :func:`~control.pole_zero_plot` for description of arguments
116+ and keywords.
117+
118+ """
119+ # If this is a root locus plot, use rlocus defaults for grid
120+ if self .loci is not None :
121+ from .rlocus import _rlocus_defaults
122+ kwargs = kwargs .copy ()
123+ kwargs ['grid' ] = config ._get_param (
124+ 'rlocus' , 'grid' , kwargs .get ('grid' , None ), _rlocus_defaults )
125+
82126 return pole_zero_plot (self , * args , ** kwargs )
83127
84128
85129class PoleZeroList (list ):
130+ """List of PoleZeroData objects."""
86131 def plot (self , * args , ** kwargs ):
132+ """Plot pole/zero data.
133+
134+ See :func:`~control.pole_zero_plot` for description of arguments
135+ and keywords.
136+
137+ """
87138 return pole_zero_plot (self , * args , ** kwargs )
88139
89140
90141# Pole/zero map
91142def pole_zero_map (sysdata ):
92- # TODO: add docstring (from old pzmap?)
143+ """Compute the pole/zero map for an LTI system.
144+
145+ Parameters
146+ ----------
147+ sys : LTI system (StateSpace or TransferFunction)
148+ Linear system for which poles and zeros are computed.
149+
150+ Returns
151+ -------
152+ pzmap_data : PoleZeroMap
153+ Pole/zero map containing the poles and zeros of the system. Use
154+ `pzmap_data.plot()` or `pole_zero_plot(pzmap_data)` to plot the
155+ pole/zero map.
156+
157+ """
93158 # Convert the first argument to a list
94159 syslist = sysdata if isinstance (sysdata , (list , tuple )) else [sysdata ]
95160
@@ -113,7 +178,6 @@ def pole_zero_plot(
113178 marker_size = None , marker_width = None , legend_loc = 'upper right' ,
114179 xlim = None , ylim = None , interactive = None , ax = None , scaling = None ,
115180 initial_gain = None , ** kwargs ):
116- # TODO: update docstring (see other response/plot functions for style)
117181 """Plot a pole/zero map for a linear system.
118182
119183 If the system data include root loci, a root locus diagram for the
@@ -137,7 +201,7 @@ def pole_zero_plot(
137201 (legacy) If ``True`` a graph is generated with Matplotlib,
138202 otherwise the poles and zeros are only computed and returned.
139203 If this argument is present, the legacy value of poles and
140- zero is returned.
204+ zeros is returned.
141205
142206 Returns
143207 -------
@@ -287,6 +351,7 @@ def pole_zero_plot(
287351
288352 # Plot the responses (and keep track of axes limits)
289353 xlim , ylim = ax .get_xlim (), ax .get_ylim ()
354+ loci_count = 0
290355 for idx , response in enumerate (pzmap_responses ):
291356 poles = response .poles
292357 zeros = response .zeros
@@ -331,9 +396,17 @@ def pole_zero_plot(
331396
332397 # TODO: add arrows to root loci (reuse Nyquist arrow code?)
333398
334- # Set up the limits for the plot
335- ax .set_xlim (xlim if xlim_user is None else xlim_user )
336- ax .set_ylim (ylim if ylim_user is None else ylim_user )
399+ # Set the axis limits to something reasonable
400+ if any ([response .loci is not None for response in pzmap_responses ]):
401+ # Set up the limits for the plot using information from loci
402+ ax .set_xlim (xlim if xlim_user is None else xlim_user )
403+ ax .set_ylim (ylim if ylim_user is None else ylim_user )
404+ else :
405+ # No root loci => only set axis limits if users specified them
406+ if xlim_user is not None :
407+ ax .set_xlim (xlim_user )
408+ if ylim_user is not None :
409+ ax .set_ylim (ylim_user )
337410
338411 # List of systems that are included in this plot
339412 lines , labels = _get_line_labels (ax )
@@ -409,7 +482,6 @@ def _click_dispatcher(event):
409482
410483
411484# Utility function to find gain corresponding to a click event
412- # TODO: project onto the root locus plot (here or above?)
413485def _find_root_locus_gain (event , sys , ax ):
414486 # Get the current axis limits to set various thresholds
415487 xlim , ylim = ax .get_xlim (), ax .get_ylim ()
@@ -469,7 +541,6 @@ def _mark_root_locus_gain(ax, sys, K):
469541
470542
471543# Return a string identifying a clicked point
472- # TODO: project onto the root locus plot (here or above?)
473544def _create_root_locus_label (sys , K , s ):
474545 # Figure out the damping ratio
475546 if isdtime (sys , strict = True ):
@@ -482,7 +553,6 @@ def _create_root_locus_label(sys, K, s):
482553
483554
484555# Utility function to compute limits for root loci
485- # TODO: (note that sys is now available => code here may not be needed)
486556def _compute_root_locus_limits (response ):
487557 loci = response .loci
488558
0 commit comments