1
+ """nichols.py
2
+
3
+ Functions for plotting Black-Nichols charts.
4
+
5
+ Routines in this module:
6
+
7
+ nichols.nichols_plot aliased as nichols.nichols
8
+ nichols.nichols_grid
9
+ """
10
+
1
11
# nichols.py - Nichols plot
2
12
#
3
13
# Contributed by Allan McInnes <Allan.McInnes@canterbury.ac.nz>
45
55
from .ctrlutil import unwrap
46
56
from .freqplot import default_frequency_range
47
57
48
- __all__ = ['nichols_plot' , 'nichols' ]
58
+ __all__ = ['nichols_plot' , 'nichols' , 'nichols_grid' ]
59
+
49
60
50
- # Nichols plot
51
- def nichols_plot (syslist , omega = None , grid = True ):
61
+ def nichols_plot (sys_list , omega = None , grid = True ):
52
62
"""Nichols plot for a system
53
63
54
64
Plots a Nichols plot for the system over a (optional) frequency range.
55
65
56
66
Parameters
57
67
----------
58
- syslist : list of LTI, or LTI
68
+ sys_list : list of LTI, or LTI
59
69
List of linear input/output systems (single system is OK)
60
70
omega : array_like
61
71
Range of frequencies (list or bounds) in rad/sec
@@ -68,14 +78,14 @@ def nichols_plot(syslist, omega=None, grid=True):
68
78
"""
69
79
70
80
# If argument was a singleton, turn it into a list
71
- if ( not getattr (syslist , '__iter__' , False ) ):
72
- syslist = (syslist ,)
81
+ if not getattr (sys_list , '__iter__' , False ):
82
+ sys_list = (sys_list ,)
73
83
74
84
# Select a default range if none is provided
75
85
if omega is None :
76
- omega = default_frequency_range (syslist )
86
+ omega = default_frequency_range (sys_list )
77
87
78
- for sys in syslist :
88
+ for sys in sys_list :
79
89
# Get the magnitude and phase of the system
80
90
mag_tmp , phase_tmp , omega = sys .freqresp (omega )
81
91
mag = np .squeeze (mag_tmp )
@@ -100,9 +110,8 @@ def nichols_plot(syslist, omega=None, grid=True):
100
110
if grid :
101
111
nichols_grid ()
102
112
103
- # Nichols grid
104
- #! TODO: Consider making linestyle configurable
105
- def nichols_grid (cl_mags = None , cl_phases = None ):
113
+
114
+ def nichols_grid (cl_mags = None , cl_phases = None , line_style = 'dotted' ):
106
115
"""Nichols chart grid
107
116
108
117
Plots a Nichols chart grid on the current axis, or creates a new chart
@@ -116,6 +125,8 @@ def nichols_grid(cl_mags=None, cl_phases=None):
116
125
cl_phases : array-like (degrees), optional
117
126
Array of closed-loop phases defining the iso-phase lines on a custom
118
127
Nichols chart. Must be in the range -360 < cl_phases < 0
128
+ line_style : string, optional
129
+ .. seealso:: https://matplotlib.org/gallery/lines_bars_and_markers/linestyles.html
119
130
120
131
Returns
121
132
-------
@@ -137,12 +148,12 @@ def nichols_grid(cl_mags=None, cl_phases=None):
137
148
# The key set of magnitudes are always generated, since this
138
149
# guarantees a recognizable Nichols chart grid.
139
150
key_cl_mags = np .array ([- 40.0 , - 20.0 , - 12.0 , - 6.0 , - 3.0 , - 1.0 , - 0.5 , 0.0 ,
140
- 0.25 , 0.5 , 1.0 , 3.0 , 6.0 , 12.0 ])
151
+ 0.25 , 0.5 , 1.0 , 3.0 , 6.0 , 12.0 ])
141
152
# Extend the range of magnitudes if necessary. The extended arange
142
153
# will end up empty if no extension is required. Assumes that closed-loop
143
154
# magnitudes are approximately aligned with open-loop magnitudes beyond
144
155
# the value of np.min(key_cl_mags)
145
- cl_mag_step = - 20.0 # dB
156
+ cl_mag_step = - 20.0 # dB
146
157
extended_cl_mags = np .arange (np .min (key_cl_mags ),
147
158
ol_mag_min + cl_mag_step , cl_mag_step )
148
159
cl_mags = np .concatenate ((extended_cl_mags , key_cl_mags ))
@@ -163,12 +174,12 @@ def nichols_grid(cl_mags=None, cl_phases=None):
163
174
# Find the M-contours
164
175
m = m_circles (cl_mags , phase_min = np .min (cl_phases ), phase_max = np .max (cl_phases ))
165
176
m_mag = 20 * sp .log10 (np .abs (m ))
166
- m_phase = sp .mod (sp .degrees (sp .angle (m )), - 360.0 ) # Unwrap
177
+ m_phase = sp .mod (sp .degrees (sp .angle (m )), - 360.0 ) # Unwrap
167
178
168
179
# Find the N-contours
169
180
n = n_circles (cl_phases , mag_min = np .min (cl_mags ), mag_max = np .max (cl_mags ))
170
181
n_mag = 20 * sp .log10 (np .abs (n ))
171
- n_phase = sp .mod (sp .degrees (sp .angle (n )), - 360.0 ) # Unwrap
182
+ n_phase = sp .mod (sp .degrees (sp .angle (n )), - 360.0 ) # Unwrap
172
183
173
184
# Plot the contours behind other plot elements.
174
185
# The "phase offset" is used to produce copies of the chart that cover
@@ -182,10 +193,10 @@ def nichols_grid(cl_mags=None, cl_phases=None):
182
193
183
194
for phase_offset in phase_offsets :
184
195
# Draw M and N contours
185
- plt .plot (m_phase + phase_offset , m_mag , color = 'gray ' ,
186
- linestyle = 'dotted' , zorder = 0 )
187
- plt .plot (n_phase + phase_offset , n_mag , color = 'gray ' ,
188
- linestyle = 'dotted' , zorder = 0 )
196
+ plt .plot (m_phase + phase_offset , m_mag , color = 'lightgray ' ,
197
+ linestyle = line_style , zorder = 0 )
198
+ plt .plot (n_phase + phase_offset , n_mag , color = 'lightgray ' ,
199
+ linestyle = line_style , zorder = 0 )
189
200
190
201
# Add magnitude labels
191
202
for x , y , m in zip (m_phase [:][- 1 ] + phase_offset , m_mag [:][- 1 ], cl_mags ):
@@ -203,7 +214,7 @@ def nichols_grid(cl_mags=None, cl_phases=None):
203
214
# generating Nichols plots
204
215
#
205
216
206
- # Compute contours of a closed-loop transfer function
217
+
207
218
def closed_loop_contours (Gcl_mags , Gcl_phases ):
208
219
"""Contours of the function Gcl = Gol/(1+Gol), where
209
220
Gol is an open-loop transfer function, and Gcl is a corresponding
@@ -229,7 +240,7 @@ def closed_loop_contours(Gcl_mags, Gcl_phases):
229
240
# Invert Gcl = Gol/(1+Gol) to map the contours into the open-loop space
230
241
return Gcl / (1.0 - Gcl )
231
242
232
- # M-circle
243
+
233
244
def m_circles (mags , phase_min = - 359.75 , phase_max = - 0.25 ):
234
245
"""Constant-magnitude contours of the function Gcl = Gol/(1+Gol), where
235
246
Gol is an open-loop transfer function, and Gcl is a corresponding
@@ -255,7 +266,7 @@ def m_circles(mags, phase_min=-359.75, phase_max=-0.25):
255
266
Gcl_mags , Gcl_phases = sp .meshgrid (10.0 ** (mags / 20.0 ), phases )
256
267
return closed_loop_contours (Gcl_mags , Gcl_phases )
257
268
258
- # N-circle
269
+
259
270
def n_circles (phases , mag_min = - 40.0 , mag_max = 12.0 ):
260
271
"""Constant-phase contours of the function Gcl = Gol/(1+Gol), where
261
272
Gol is an open-loop transfer function, and Gcl is a corresponding
@@ -281,5 +292,6 @@ def n_circles(phases, mag_min=-40.0, mag_max=12.0):
281
292
Gcl_phases , Gcl_mags = sp .meshgrid (sp .radians (phases ), mags )
282
293
return closed_loop_contours (Gcl_mags , Gcl_phases )
283
294
295
+
284
296
# Function aliases
285
297
nichols = nichols_plot
0 commit comments