51
51
52
52
import numpy as np
53
53
import matplotlib .pyplot as plt
54
+ import matplotlib .transforms
55
+
54
56
from .ctrlutil import unwrap
55
57
from .freqplot import _default_frequency_range
56
58
from . import config
@@ -119,7 +121,18 @@ def nichols_plot(sys_list, omega=None, grid=None):
119
121
nichols_grid ()
120
122
121
123
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 ):
123
136
"""Nichols chart grid
124
137
125
138
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'):
136
149
line_style : string, optional
137
150
:doc:`Matplotlib linestyle \
138
151
<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.
140
156
"""
157
+ if ax is None :
158
+ ax = plt .gca ()
159
+
141
160
# Default chart size
142
161
ol_phase_min = - 359.99
143
162
ol_phase_max = 0.0
144
163
ol_mag_min = - 40.0
145
164
ol_mag_max = default_ol_mag_max = 50.0
146
165
147
166
# 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 )
150
169
151
170
# M-circle magnitudes.
152
171
if cl_mags is None :
@@ -165,17 +184,18 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'):
165
184
ol_mag_min + cl_mag_step , cl_mag_step )
166
185
cl_mags = np .concatenate ((extended_cl_mags , key_cl_mags ))
167
186
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
+
168
190
# N-circle phases (should be in the range -360 to 0)
169
191
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 )))
179
199
else :
180
200
assert ((- 360.0 < np .min (cl_phases )) and (np .max (cl_phases ) < 0.0 ))
181
201
@@ -196,27 +216,46 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'):
196
216
# over the range -360 < phase < 0. Given the range
197
217
# the base chart is computed over, the phase offset should be 0
198
218
# 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
201
219
phase_offsets = np .arange (phase_offset_min , phase_offset_max , 360.0 )
202
220
203
221
for phase_offset in phase_offsets :
204
222
# 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' ,
206
224
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' ,
208
226
linestyle = line_style , zorder = 0 )
209
227
210
228
# Add magnitude labels
211
229
for x , y , m in zip (m_phase [:][- 1 ] + phase_offset , m_mag [:][- 1 ],
212
230
cl_mags ):
213
231
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
+
216
253
217
254
# 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 ])])
220
259
221
260
#
222
261
# Utility functions
0 commit comments