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

Commitc383014

Browse files
committed
add frequency_limit and line style processing + unit tests, fixes
1 parentde5ea5a commitc383014

File tree

4 files changed

+179
-75
lines changed

4 files changed

+179
-75
lines changed

‎control/freqplot.py

Lines changed: 98 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@
77
# Nyquist plots and other frequency response plots. The code for Nichols
88
# charts is in nichols.py. The code for pole-zero diagrams is in pzmap.py
99
# and rlocus.py.
10-
#
11-
# Functionality to add/check (Jul 2023, working list)
12-
# [?] Allow line colors/styles to be set in plot() command (also time plots)
13-
# [ ] Get sisotool working in iPython and document how to make it work
14-
# [ ] Allow use of subplot labels instead of output/input subtitles
15-
# [i] Allow frequency range to be overridden in bode_plot
16-
# [i] Unit tests for discrete time systems with different sample times
17-
# [ ] Add unit tests for ct.config.defaults['freqplot_number_of_samples']
1810

1911
importnumpyasnp
2012
importmatplotlibasmpl
@@ -103,7 +95,7 @@ def bode_plot(
10395
overlay_outputs=None,overlay_inputs=None,phase_label=None,
10496
magnitude_label=None,display_margins=None,
10597
margins_method='best',legend_map=None,legend_loc=None,
106-
sharex=None,sharey=None,title=None,relabel=True,**kwargs):
98+
sharex=None,sharey=None,title=None,**kwargs):
10799
"""Bode plot for a system.
108100
109101
Plot the magnitude and phase of the frequency response over a
@@ -116,7 +108,8 @@ def bode_plot(
116108
single system or frequency response can also be passed.
117109
omega : array_like, optoinal
118110
List of frequencies in rad/sec over to plot over. If not specified,
119-
this will be determined from the proporties of the systems.
111+
this will be determined from the proporties of the systems. Ignored
112+
if `data` is not a list of systems.
120113
*fmt : :func:`matplotlib.pyplot.plot` format string, optional
121114
Passed to `matplotlib` as the format string for all lines in the plot.
122115
The `omega` parameter must be present (use omega=None if needed).
@@ -147,16 +140,6 @@ def bode_plot(
147140
148141
Other Parameters
149142
----------------
150-
plot : bool, optional
151-
(legacy) If given, `bode_plot` returns the legacy return values
152-
of magnitude, phase, and frequency. If False, just return the
153-
values with no plot.
154-
omega_limits : array_like of two values
155-
Limits of the to generate frequency vector. If Hz=True the limits
156-
are in Hz otherwise in rad/s.
157-
omega_num : int
158-
Number of samples to plot. Defaults to
159-
config.defaults['freqplot.number_of_samples'].
160143
grid : bool
161144
If True, plot grid lines on gain and phase plots. Default is set by
162145
`config.defaults['freqplot.grid']`.
@@ -166,6 +149,20 @@ def bode_plot(
166149
value specified. Units are in either degrees or radians, depending on
167150
the `deg` parameter. Default is -180 if wrap_phase is False, 0 if
168151
wrap_phase is True.
152+
omega_limits : array_like of two values
153+
Set limits for plotted frequency range. If Hz=True the limits
154+
are in Hz otherwise in rad/s.
155+
omega_num : int
156+
Number of samples to use for the frequeny range. Defaults to
157+
config.defaults['freqplot.number_of_samples']. Ignore if data is
158+
not a list of systems.
159+
plot : bool, optional
160+
(legacy) If given, `bode_plot` returns the legacy return values
161+
of magnitude, phase, and frequency. If False, just return the
162+
values with no plot.
163+
rcParams : dict
164+
Override the default parameters used for generating plots.
165+
Default is set up config.default['freqplot.rcParams'].
169166
wrap_phase : bool or float
170167
If wrap_phase is `False` (default), then the phase will be unwrapped
171168
so that it is continuously increasing or decreasing. If wrap_phase is
@@ -220,7 +217,6 @@ def bode_plot(
220217
'freqplot','wrap_phase',kwargs,_freqplot_defaults,pop=True)
221218
initial_phase=config._get_param(
222219
'freqplot','initial_phase',kwargs,None,pop=True)
223-
omega_num=config._get_param('freqplot','number_of_samples',omega_num)
224220
freqplot_rcParams=config._get_param(
225221
'freqplot','rcParams',kwargs,_freqplot_defaults,pop=True)
226222

@@ -262,7 +258,7 @@ def bode_plot(
262258
data= [data]
263259

264260
#
265-
# Pre-process the data to be plotted (unwrap phase)
261+
# Pre-process the data to be plotted (unwrap phase, limit frequencies)
266262
#
267263
# To maintain compatibility with legacy uses of bode_plot(), we do some
268264
# initial processing on the data, specifically phase unwrapping and
@@ -277,6 +273,17 @@ def bode_plot(
277273
data=frequency_response(
278274
data,omega=omega,omega_limits=omega_limits,
279275
omega_num=omega_num,Hz=Hz)
276+
else:
277+
# Generate warnings if frequency keywords were given
278+
ifomega_numisnotNone:
279+
warnings.warn("`omega_num` ignored when passed response data")
280+
elifomegaisnotNone:
281+
warnings.warn("`omega` ignored when passed response data")
282+
283+
# Check to make sure omega_limits is sensible
284+
ifomega_limitsisnotNoneand \
285+
(len(omega_limits)!=2oromega_limits[1]<=omega_limits[0]):
286+
raiseValueError(f"invalid limits:{omega_limits=}")
280287

281288
# If plot_phase is not specified, check the data first, otherwise true
282289
ifplot_phaseisNone:
@@ -288,7 +295,6 @@ def bode_plot(
288295

289296
mag_data,phase_data,omega_data= [], [], []
290297
forresponseindata:
291-
phase=response.phase.copy()
292298
noutputs,ninputs=response.noutputs,response.ninputs
293299

294300
ifinitial_phaseisNone:
@@ -306,9 +312,9 @@ def bode_plot(
306312
raiseValueError("initial_phase must be a number.")
307313

308314
# Reshape the phase to allow standard indexing
309-
phase=phase.reshape((noutputs,ninputs,-1))
315+
phase=response.phase.copy().reshape((noutputs,ninputs,-1))
310316

311-
# Shift and wrap
317+
# Shift and wrap the phase
312318
fori,jinitertools.product(range(noutputs),range(ninputs)):
313319
# Shift the phase if needed
314320
ifabs(phase[i,j,0]-initial_phase_value)>math.pi:
@@ -641,7 +647,7 @@ def _make_line_label(response, output_index, input_index):
641647
# Get the (pre-processed) data in fully indexed form
642648
mag=mag_data[index].reshape((noutputs,ninputs,-1))
643649
phase=phase_data[index].reshape((noutputs,ninputs,-1))
644-
omega_sys,sysname=response.omega,response.sysname
650+
omega_sys,sysname=omega_data[index],response.sysname
645651

646652
fori,jinitertools.product(range(noutputs),range(ninputs)):
647653
# Get the axes to use for magnitude and phase
@@ -831,21 +837,17 @@ def _make_line_label(response, output_index, input_index):
831837

832838
foriinrange(noutputs):
833839
forjinrange(ninputs):
840+
# Utility function to generate phase labels
834841
defgen_zero_centered_series(val_min,val_max,period):
835842
v1=np.ceil(val_min/period-0.2)
836843
v2=np.floor(val_max/period+0.2)
837844
returnnp.arange(v1,v2+1)*period
838845

839-
# TODO: put Nyquist lines here?
840-
841-
# TODO: what is going on here
842-
# TODO: fix to use less dense labels, when needed
843-
# TODO: make sure turning sharey on and off makes labels come/go
846+
# Label the phase axes using multiples of 45 degrees
844847
ifplot_phase:
845848
ax_phase=ax_array[phase_map[i,j]]
846849

847850
# Set the labels
848-
# TODO: tighten up code
849851
ifdeg:
850852
ylim=ax_phase.get_ylim()
851853
num=np.floor((ylim[1]-ylim[0])/45)
@@ -877,6 +879,11 @@ def gen_zero_centered_series(val_min, val_max, period):
877879
ifshare_frequencyin [True,'all','col']:
878880
ax_array[i,j].tick_params(labelbottom=False)
879881

882+
# If specific omega_limits were given, use them
883+
ifomega_limitsisnotNone:
884+
fori,jinitertools.product(range(nrows),range(ncols)):
885+
ax_array[i,j].set_xlim(omega_limits)
886+
880887
#
881888
# Update the plot title (= figure suptitle)
882889
#
@@ -895,7 +902,7 @@ def gen_zero_centered_series(val_min, val_max, period):
895902
else:
896903
title=data[0].title
897904

898-
iffigisnotNoneandtitleisnotNone:
905+
iffigisnotNoneandisinstance(title,str):
899906
# Get the current title, if it exists
900907
old_title=Noneiffig._suptitleisNoneelsefig._suptitle._text
901908
new_title=title
@@ -1294,11 +1301,11 @@ def nyquist_response(
12941301
# Determine the contour used to evaluate the Nyquist curve
12951302
ifsys.isdtime(strict=True):
12961303
# Restrict frequencies for discrete-time systems
1297-
nyquistfrq=math.pi/sys.dt
1304+
nyq_freq=math.pi/sys.dt
12981305
ifnotomega_range_given:
12991306
# limit up to and including Nyquist frequency
13001307
omega_sys=np.hstack((
1301-
omega_sys[omega_sys<nyquistfrq],nyquistfrq))
1308+
omega_sys[omega_sys<nyq_freq],nyq_freq))
13021309

13031310
# Issue a warning if we are sampling above Nyquist
13041311
ifnp.any(omega_sys*sys.dt>np.pi)andwarn_nyquist:
@@ -1817,7 +1824,7 @@ def _parse_linestyle(style_name, allow_false=False):
18171824
x,y=resp.real.copy(),resp.imag.copy()
18181825
x[reg_mask]*= (1+curve_offset[reg_mask])
18191826
y[reg_mask]*= (1+curve_offset[reg_mask])
1820-
p=plt.plot(x,y,linestyle='None',color=c,**kwargs)
1827+
p=plt.plot(x,y,linestyle='None',color=c)
18211828

18221829
# Add arrows
18231830
ax=plt.gca()
@@ -2210,10 +2217,6 @@ def singular_values_plot(
22102217
Hz : bool
22112218
If True, plot frequency in Hz (omega must be provided in rad/sec).
22122219
Default value (False) set by config.defaults['freqplot.Hz'].
2213-
plot : bool, optional
2214-
(legacy) If given, `singular_values_plot` returns the legacy return
2215-
values of magnitude, phase, and frequency. If False, just return
2216-
the values with no plot.
22172220
legend_loc : str, optional
22182221
For plots with multiple lines, a legend will be included in the
22192222
given location. Default is 'center right'. Use False to supress.
@@ -2233,6 +2236,26 @@ def singular_values_plot(
22332236
omega : ndarray (or list of ndarray if len(data) > 1))
22342237
If plot=False, frequency in rad/sec (deprecated).
22352238
2239+
Other Parameters
2240+
----------------
2241+
grid : bool
2242+
If True, plot grid lines on gain and phase plots. Default is set by
2243+
`config.defaults['freqplot.grid']`.
2244+
omega_limits : array_like of two values
2245+
Set limits for plotted frequency range. If Hz=True the limits
2246+
are in Hz otherwise in rad/s.
2247+
omega_num : int
2248+
Number of samples to use for the frequeny range. Defaults to
2249+
config.defaults['freqplot.number_of_samples']. Ignore if data is
2250+
not a list of systems.
2251+
plot : bool, optional
2252+
(legacy) If given, `singular_values_plot` returns the legacy return
2253+
values of magnitude, phase, and frequency. If False, just return
2254+
the values with no plot.
2255+
rcParams : dict
2256+
Override the default parameters used for generating plots.
2257+
Default is set up config.default['freqplot.rcParams'].
2258+
22362259
"""
22372260
# Keyword processing
22382261
dB=config._get_param(
@@ -2241,7 +2264,6 @@ def singular_values_plot(
22412264
'freqplot','Hz',kwargs,_freqplot_defaults,pop=True)
22422265
grid=config._get_param(
22432266
'freqplot','grid',kwargs,_freqplot_defaults,pop=True)
2244-
omega_num=config._get_param('freqplot','number_of_samples',omega_num)
22452267
freqplot_rcParams=config._get_param(
22462268
'freqplot','rcParams',kwargs,_freqplot_defaults,pop=True)
22472269

@@ -2255,6 +2277,17 @@ def singular_values_plot(
22552277
data,omega=omega,omega_limits=omega_limits,
22562278
omega_num=omega_num)
22572279
else:
2280+
# Generate warnings if frequency keywords were given
2281+
ifomega_numisnotNone:
2282+
warnings.warn("`omega_num` ignored when passed response data")
2283+
elifomegaisnotNone:
2284+
warnings.warn("`omega` ignored when passed response data")
2285+
2286+
# Check to make sure omega_limits is sensible
2287+
ifomega_limitsisnotNoneand \
2288+
(len(omega_limits)!=2oromega_limits[1]<=omega_limits[0]):
2289+
raiseValueError(f"invalid limits:{omega_limits=}")
2290+
22582291
responses=data
22592292

22602293
# Process (legacy) plot keyword
@@ -2308,48 +2341,49 @@ def singular_values_plot(
23082341
# Create a list of lines for the output
23092342
out=np.empty(len(data),dtype=object)
23102343

2344+
# Plot the singular values for each response
23112345
foridx_sys,responseinenumerate(responses):
23122346
sigma=sigmas[idx_sys].transpose()# frequency first for plotting
2313-
omega_sys=omegas[idx_sys]
2347+
omega=omegas[idx_sys]/ (2*math.pi)ifHzelseomegas[idx_sys]
2348+
23142349
ifresponse.isdtime(strict=True):
2315-
nyquistfrq=math.pi/response.dt
2350+
nyq_freq=(0.5/response.dt)ifHzelse (math.pi/response.dt)
23162351
else:
2317-
nyquistfrq=None
2352+
nyq_freq=None
23182353

2319-
color=color_cycle[(idx_sys+color_offset)%len(color_cycle)]
2320-
color=kwargs.pop('color',color)
2321-
2322-
# TODO: copy from above
2323-
nyquistfrq_plot=None
2324-
ifHz:
2325-
omega_plot=omega_sys/ (2.*math.pi)
2326-
ifnyquistfrq:
2327-
nyquistfrq_plot=nyquistfrq/ (2.*math.pi)
2354+
# See if the color was specified, otherwise rotate
2355+
ifkwargs.get('color',None)orany(
2356+
[isinstance(arg,str)and
2357+
any([cinargforcin"bgrcmykw#"])forarginfmt]):
2358+
color_arg= {}# color set by *fmt, **kwargs
23282359
else:
2329-
omega_plot=omega_sys
2330-
ifnyquistfrq:
2331-
nyquistfrq_plot=nyquistfrq
2332-
sigma_plot=sigma
2360+
color_arg= {'color':color_cycle[
2361+
(idx_sys+color_offset)%len(color_cycle)]}
23332362

23342363
# Decide on the system name
23352364
sysname=response.sysnameifresponse.sysnameisnotNone \
23362365
elsef"Unknown-{idx_sys}"
23372366

2367+
# Plot the data
23382368
ifdB:
23392369
withplt.rc_context(freqplot_rcParams):
23402370
out[idx_sys]=ax_sigma.semilogx(
2341-
omega_plot,20*np.log10(sigma_plot),*fmt,color=color,
2342-
label=sysname,**kwargs)
2371+
omega,20*np.log10(sigma),*fmt,
2372+
label=sysname,**color_arg,**kwargs)
23432373
else:
23442374
withplt.rc_context(freqplot_rcParams):
23452375
out[idx_sys]=ax_sigma.loglog(
2346-
omega_plot,sigma_plot,color=color,label=sysname,
2347-
*fmt,**kwargs)
2376+
omega,sigma,label=sysname,*fmt,**color_arg,**kwargs)
23482377

2349-
ifnyquistfrq_plotisnotNone:
2378+
# Plot the Nyquist frequency
2379+
ifnyq_freqisnotNone:
23502380
ax_sigma.axvline(
2351-
nyquistfrq_plot,color=color,linestyle='--',
2352-
label='_nyq_freq_'+sysname)
2381+
nyq_freq,linestyle='--',label='_nyq_freq_'+sysname,
2382+
**color_arg)
2383+
2384+
# If specific omega_limits were given, use them
2385+
ifomega_limitsisnotNone:
2386+
ax_sigma.set_xlim(omega_limits)
23532387

23542388
# Add a grid to the plot + labeling
23552389
ifgrid:

‎control/sisotool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect=None):
149149

150150
# Update the bodeplot
151151
bode_plot_params['data']=frequency_response(sys_loop*K.real)
152-
bode_plot(**bode_plot_params)
152+
bode_plot(**bode_plot_params,title=False)
153153

154154
# Set the titles and labels
155155
ax_mag.set_title('Bode magnitude',fontsize=title_font_size)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp