Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitae8d586

Browse files
authored
Merge pull request#723 from roryyorke/rory/nichols-improvements
Improvements to Nichols chart plotting
2 parentsdd95f35 +59cd872 commitae8d586

File tree

2 files changed

+159
-27
lines changed

2 files changed

+159
-27
lines changed

‎control/nichols.py

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151

5252
importnumpyasnp
5353
importmatplotlib.pyplotasplt
54+
importmatplotlib.transforms
55+
5456
from .ctrlutilimportunwrap
5557
from .freqplotimport_default_frequency_range
5658
from .importconfig
@@ -119,7 +121,18 @@ def nichols_plot(sys_list, omega=None, grid=None):
119121
nichols_grid()
120122

121123

122-
defnichols_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_innerisNone:
129+
returnax.ViewLim.extents
130+
else:
131+
return_inner.extents
132+
133+
134+
defnichols_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,36 @@ 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>`
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.
139156
157+
Returns
158+
-------
159+
cl_mag_lines: list of `matplotlib.line.Line2D`
160+
The constant closed-loop gain contours
161+
cl_phase_lines: list of `matplotlib.line.Line2D`
162+
The constant closed-loop phase contours
163+
cl_mag_labels: list of `matplotlib.text.Text`
164+
mcontour labels; each entry corresponds to the respective entry
165+
in ``cl_mag_lines``
166+
cl_phase_labels: list of `matplotlib.text.Text`
167+
ncontour labels; each entry corresponds to the respective entry
168+
in ``cl_phase_lines``
140169
"""
170+
ifaxisNone:
171+
ax=plt.gca()
172+
141173
# Default chart size
142174
ol_phase_min=-359.99
143175
ol_phase_max=0.0
144176
ol_mag_min=-40.0
145177
ol_mag_max=default_ol_mag_max=50.0
146178

147-
# Find bounds of the current dataset,ifthere is one.
148-
ifplt.gcf().gca().has_data():
149-
ol_phase_min,ol_phase_max,ol_mag_min,ol_mag_max=plt.axis()
179+
ifax.has_data():
180+
# Find extent of intersection the current dataset or view
181+
ol_phase_min,ol_mag_min,ol_phase_max,ol_mag_max=_inner_extents(ax)
150182

151183
# M-circle magnitudes.
152184
ifcl_magsisNone:
@@ -165,19 +197,22 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'):
165197
ol_mag_min+cl_mag_step,cl_mag_step)
166198
cl_mags=np.concatenate((extended_cl_mags,key_cl_mags))
167199

200+
# a minimum 360deg extent containing the phases
201+
phase_round_max=360.0*np.ceil(ol_phase_max/360.0)
202+
phase_round_min=min(phase_round_max-360,
203+
360.0*np.floor(ol_phase_min/360.0))
204+
168205
# N-circle phases (should be in the range -360 to 0)
169206
ifcl_phasesisNone:
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-
ifnp.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))
179-
else:
180-
assert ((-360.0<np.min(cl_phases))and (np.max(cl_phases)<0.0))
207+
# aim for 9 lines, but always show (-360+eps, -180, -eps)
208+
# smallest spacing is 45, biggest is 180
209+
phase_span=phase_round_max-phase_round_min
210+
spacing=np.clip(round(phase_span/8/45)*45,45,180)
211+
key_cl_phases=np.array([-0.25,-359.75])
212+
other_cl_phases=np.arange(-spacing,-360.0,-spacing)
213+
cl_phases=np.unique(np.concatenate((key_cl_phases,other_cl_phases)))
214+
elifnot ((-360<np.min(cl_phases))and (np.max(cl_phases)<0.0)):
215+
raiseValueError('cl_phases must between -360 and 0, exclusive')
181216

182217
# Find the M-contours
183218
m=m_circles(cl_mags,phase_min=np.min(cl_phases),
@@ -196,27 +231,57 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'):
196231
# over the range -360 < phase < 0. Given the range
197232
# the base chart is computed over, the phase offset should be 0
198233
# 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-
phase_offsets=np.arange(phase_offset_min,phase_offset_max,360.0)
234+
phase_offsets=360+np.arange(phase_round_min,phase_round_max,360.0)
235+
236+
cl_mag_lines= []
237+
cl_phase_lines= []
238+
cl_mag_labels= []
239+
cl_phase_labels= []
202240

203241
forphase_offsetinphase_offsets:
204242
# Draw M and N contours
205-
plt.plot(m_phase+phase_offset,m_mag,color='lightgray',
206-
linestyle=line_style,zorder=0)
207-
plt.plot(n_phase+phase_offset,n_mag,color='lightgray',
208-
linestyle=line_style,zorder=0)
243+
cl_mag_lines.extend(
244+
ax.plot(m_phase+phase_offset,m_mag,color='lightgray',
245+
linestyle=line_style,zorder=0))
246+
cl_phase_lines.extend(
247+
ax.plot(n_phase+phase_offset,n_mag,color='lightgray',
248+
linestyle=line_style,zorder=0))
209249

210250
# Add magnitude labels
211251
forx,y,minzip(m_phase[:][-1]+phase_offset,m_mag[:][-1],
212252
cl_mags):
213253
align='right'ifm<0.0else'left'
214-
plt.text(x,y,str(m)+' dB',size='small',ha=align,
215-
color='gray')
254+
cl_mag_labels.append(
255+
ax.text(x,y,str(m)+' dB',size='small',ha=align,
256+
color='gray',clip_on=True))
257+
258+
# phase labels
259+
iflabel_cl_phases:
260+
forx,y,pinzip(n_phase[:][0]+phase_offset,
261+
n_mag[:][0],
262+
cl_phases):
263+
ifp>-175:
264+
align='right'
265+
elifp>-185:
266+
align='center'
267+
else:
268+
align='left'
269+
cl_phase_labels.append(
270+
ax.text(x,y,f'{round(p)}\N{DEGREE SIGN}',
271+
size='small',
272+
ha=align,
273+
va='bottom',
274+
color='gray',
275+
clip_on=True))
276+
216277

217278
# 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])])
279+
ax.axis([phase_round_min,
280+
phase_round_max,
281+
np.min(np.concatenate([cl_mags,[ol_mag_min]])),
282+
np.max([ol_mag_max,default_ol_mag_max])])
283+
284+
returncl_mag_lines,cl_phase_lines,cl_mag_labels,cl_phase_labels
220285

221286
#
222287
# Utility functions

‎control/tests/nichols_test.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
RMM, 31 Mar 2011
44
"""
55

6+
importmatplotlib.pyplotasplt
7+
68
importpytest
79

8-
fromcontrolimportStateSpace,nichols_plot,nichols
10+
fromcontrolimportStateSpace,nichols_plot,nichols,nichols_grid,pade,tf
911

1012

1113
@pytest.fixture()
@@ -26,3 +28,68 @@ def test_nichols(tsys, mplcleanup):
2628
deftest_nichols_alias(tsys,mplcleanup):
2729
"""Test the control.nichols alias and the grid=False parameter"""
2830
nichols(tsys,grid=False)
31+
32+
33+
@pytest.mark.usefixtures("mplcleanup")
34+
classTestNicholsGrid:
35+
deftest_ax(self):
36+
# check grid is plotted into gca, or specified axis
37+
fig,axs=plt.subplots(2,2)
38+
plt.sca(axs[0,1])
39+
40+
cl_mag_lines=nichols_grid()[1]
41+
assertcl_mag_lines[0].axesisaxs[0,1]
42+
43+
cl_mag_lines=nichols_grid(ax=axs[1,1])[1]
44+
assertcl_mag_lines[0].axesisaxs[1,1]
45+
# nichols_grid didn't change what the "current axes" are
46+
assertplt.gca()isaxs[0,1]
47+
48+
49+
deftest_cl_phase_label_control(self):
50+
# test label_cl_phases argument
51+
cl_mag_lines,cl_phase_lines,cl_mag_labels,cl_phase_labels \
52+
=nichols_grid()
53+
assertlen(cl_phase_labels)>0
54+
55+
cl_mag_lines,cl_phase_lines,cl_mag_labels,cl_phase_labels \
56+
=nichols_grid(label_cl_phases=False)
57+
assertlen(cl_phase_labels)==0
58+
59+
60+
deftest_labels_clipped(self):
61+
# regression test: check that contour labels are clipped
62+
mcontours,ncontours,mlabels,nlabels=nichols_grid()
63+
assertall(ml.get_clip_on()formlinmlabels)
64+
assertall(nl.get_clip_on()fornlinnlabels)
65+
66+
67+
deftest_minimal_phase(self):
68+
# regression test: phase extent is minimal
69+
g=tf([1],[1,1])*tf([1],[1/1,2*0.1/1,1])
70+
nichols(g)
71+
ax=plt.gca()
72+
assertax.get_xlim()[1]<=0
73+
74+
75+
deftest_fixed_view(self):
76+
# respect xlim, ylim set by user
77+
g= (tf([1],[1/1,2*0.01/1,1])
78+
*tf([1],[1/100**2,2*0.001/100,1])
79+
*tf(*pade(0.01,5)))
80+
81+
# normally a broad axis
82+
nichols(g)
83+
84+
assert(plt.xlim()[0]==-1440)
85+
assert(plt.ylim()[0]<=-240)
86+
87+
nichols(g,grid=False)
88+
89+
# zoom in
90+
plt.axis([-360,0,-40,50])
91+
92+
# nichols_grid doesn't expand limits
93+
nichols_grid()
94+
assert(plt.xlim()[0]==-360)
95+
assert(plt.ylim()[1]>=-40)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp