5151
5252import numpy as np
5353import matplotlib .pyplot as plt
54+ import matplotlib .transforms
55+
5456from .ctrlutil import unwrap
5557from .freqplot import _default_frequency_range
5658from . import config
@@ -119,7 +121,18 @@ def nichols_plot(sys_list, omega=None, grid=None):
119121 nichols_grid ()
120122
121123
122- def nichols_grid (cl_mags = None , cl_phases = None , line_style = 'dotted' ):
124+ def _inner_extents (ax ):
125+ # intersection of data and view extents
126+ # if intersection empty, return view extents
127+ _inner = matplotlib .transforms .Bbox .intersection (ax .viewLim , ax .dataLim )
128+ if _inner is None :
129+ return ax .ViewLim .extents
130+ else :
131+ return _inner .extents
132+
133+
134+ def nichols_grid (cl_mags = None , cl_phases = None , line_style = 'dotted' , ax = None ,
135+ label_cl_phases = True ):
123136 """Nichols chart grid
124137
125138 Plots a Nichols chart grid on the current axis, or creates a new chart
@@ -136,17 +149,23 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'):
136149 line_style : string, optional
137150 :doc:`Matplotlib linestyle \
138151 <matplotlib:gallery/lines_bars_and_markers/linestyles>`
139-
152+ ax : matplotlib.axes.Axes, optional
153+ Axes to add grid to. If ``None``, use ``plt.gca()``.
154+ label_cl_phases: bool, optional
155+ If True, closed-loop phase lines will be labelled.
140156 """
157+ if ax is None :
158+ ax = plt .gca ()
159+
141160 # Default chart size
142161 ol_phase_min = - 359.99
143162 ol_phase_max = 0.0
144163 ol_mag_min = - 40.0
145164 ol_mag_max = default_ol_mag_max = 50.0
146165
147166 # Find bounds of the current dataset, if there is one.
148- if plt . gcf (). gca () .has_data ():
149- ol_phase_min , ol_phase_max , ol_mag_min , ol_mag_max = plt . axis ( )
167+ if ax .has_data ():
168+ ol_phase_min , ol_mag_min , ol_phase_max , ol_mag_max = _inner_extents ( ax )
150169
151170 # M-circle magnitudes.
152171 if cl_mags is None :
@@ -165,17 +184,18 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'):
165184 ol_mag_min + cl_mag_step , cl_mag_step )
166185 cl_mags = np .concatenate ((extended_cl_mags , key_cl_mags ))
167186
187+ phase_offset_min = 360.0 * np .ceil (ol_phase_min / 360.0 )
188+ phase_offset_max = 360.0 * np .ceil (ol_phase_max / 360.0 ) + 360.0
189+
168190 # N-circle phases (should be in the range -360 to 0)
169191 if cl_phases is None :
170- # Choose a reasonable set of default phases (denser if the open-loop
171- # data is restricted to a relatively small range of phases).
172- key_cl_phases = np .array ([- 0.25 , - 45.0 , - 90.0 , - 180.0 , - 270.0 ,
173- - 325.0 , - 359.75 ])
174- if np .abs (ol_phase_max - ol_phase_min ) < 90.0 :
175- other_cl_phases = np .arange (- 10.0 , - 360.0 , - 10.0 )
176- else :
177- other_cl_phases = np .arange (- 10.0 , - 360.0 , - 20.0 )
178- cl_phases = np .concatenate ((key_cl_phases , other_cl_phases ))
192+ # aim for 9 lines, but always show (-360+eps, -180, -eps)
193+ # smallest spacing is 45, biggest is 180
194+ phase_span = phase_offset_max - phase_offset_min
195+ spacing = np .clip (round (phase_span / 8 / 45 ) * 45 , 45 , 180 )
196+ key_cl_phases = np .array ([- 0.25 , - 359.75 ])
197+ other_cl_phases = np .arange (- spacing , - 360.0 , - spacing )
198+ cl_phases = np .unique (np .concatenate ((key_cl_phases , other_cl_phases )))
179199 else :
180200 assert ((- 360.0 < np .min (cl_phases )) and (np .max (cl_phases ) < 0.0 ))
181201
@@ -196,27 +216,46 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'):
196216 # over the range -360 < phase < 0. Given the range
197217 # the base chart is computed over, the phase offset should be 0
198218 # for -360 < ol_phase_min < 0.
199- phase_offset_min = 360.0 * np .ceil (ol_phase_min / 360.0 )
200- phase_offset_max = 360.0 * np .ceil (ol_phase_max / 360.0 ) + 360.0
201219 phase_offsets = np .arange (phase_offset_min , phase_offset_max , 360.0 )
202220
203221 for phase_offset in phase_offsets :
204222 # Draw M and N contours
205- plt .plot (m_phase + phase_offset , m_mag , color = 'lightgray' ,
223+ ax .plot (m_phase + phase_offset , m_mag , color = 'lightgray' ,
206224 linestyle = line_style , zorder = 0 )
207- plt .plot (n_phase + phase_offset , n_mag , color = 'lightgray' ,
225+ ax .plot (n_phase + phase_offset , n_mag , color = 'lightgray' ,
208226 linestyle = line_style , zorder = 0 )
209227
210228 # Add magnitude labels
211229 for x , y , m in zip (m_phase [:][- 1 ] + phase_offset , m_mag [:][- 1 ],
212230 cl_mags ):
213231 align = 'right' if m < 0.0 else 'left'
214- plt .text (x , y , str (m ) + ' dB' , size = 'small' , ha = align ,
215- color = 'gray' )
232+ ax .text (x , y , str (m ) + ' dB' , size = 'small' , ha = align ,
233+ color = 'gray' , clip_on = True )
234+
235+ # phase labels
236+ if label_cl_phases :
237+ for x , y , p in zip (n_phase [:][0 ] + phase_offset ,
238+ n_mag [:][0 ],
239+ cl_phases ):
240+ if p > - 175 :
241+ align = 'right'
242+ elif p > - 185 :
243+ align = 'center'
244+ else :
245+ align = 'left'
246+ ax .text (x , y , f'{ round (p )} \N{DEGREE SIGN} ' ,
247+ size = 'small' ,
248+ ha = align ,
249+ va = 'bottom' ,
250+ color = 'gray' ,
251+ clip_on = True )
252+
216253
217254 # Fit axes to generated chart
218- plt .axis ([phase_offset_min - 360.0 , phase_offset_max - 360.0 ,
219- np .min (cl_mags ), np .max ([ol_mag_max , default_ol_mag_max ])])
255+ ax .axis ([phase_offset_min - 360.0 ,
256+ phase_offset_max - 360.0 ,
257+ np .min (np .concatenate ([cl_mags ,[ol_mag_min ]])),
258+ np .max ([ol_mag_max , default_ol_mag_max ])])
220259
221260#
222261# Utility functions
0 commit comments