Skip to content

Commit d07422d

Browse files
committed
update plot handling to allow system arguments
1 parent 6ce29da commit d07422d

6 files changed

Lines changed: 68 additions & 64 deletions

File tree

control/freqplot.py

Lines changed: 38 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,10 @@ def bode_plot(
201201
202202
Other Parameters
203203
----------------
204-
plot : bool
205-
If True (default), plot magnitude and phase.
204+
plot : bool, optional
205+
(legacy) If given, `bode_plot` returns the legacy return values
206+
of magnitude, phase, and frequency. If False, just return the
207+
values with no plot.
206208
omega_limits : array_like of two values
207209
Limits of the to generate frequency vector. If Hz=True the limits
208210
are in Hz otherwise in rad/s.
@@ -232,22 +234,20 @@ def bode_plot(
232234
233235
Notes
234236
-----
235-
1. Alternatively, you may use the lower-level methods
236-
:meth:`LTI.frequency_response` or ``sys(s)`` or ``sys(z)`` or to
237-
generate the frequency response for a single system.
237+
1. Starting with python-control version 0.10, `bode_plot`returns an
238+
array of lines instead of magnitude, phase, and frequency. To
239+
recover the # old behavior, call `bode_plot` with `plot=True`, which
240+
will force the legacy return values to be used (with a warning). To
241+
obtain just the frequency response of a system (or list of systems)
242+
without plotting, use the :func:`~control.frequency_response`
243+
command.
238244
239245
2. If a discrete time model is given, the frequency response is plotted
240246
along the upper branch of the unit circle, using the mapping ``z =
241247
exp(1j * omega * dt)`` where `omega` ranges from 0 to `pi/dt` and `dt`
242248
is the discrete timebase. If timebase not specified (``dt=True``),
243249
`dt` is set to 1.
244250
245-
3. The legacy version of this function is invoked if instead of passing
246-
frequency response data, a system (or list of systems) is passed as
247-
the first argument, or if the (deprecated) keyword `plot` is set to
248-
True or False. The return value is then given as `mag`, `phase`,
249-
`omega` for the plotted frequency response (SISO only).
250-
251251
Examples
252252
--------
253253
>>> G = ct.ss([[-1, -2], [3, -4]], [[5], [7]], [[6, 8]], [[9]])
@@ -315,18 +315,6 @@ def bode_plot(
315315
if not isinstance(data, (list, tuple)):
316316
data = [data]
317317

318-
# For backwards compatibility, allow systems in the data list
319-
if all([isinstance(
320-
sys, (StateSpace, TransferFunction)) for sys in data]):
321-
data = frequency_response(
322-
data, omega=omega, omega_limits=omega_limits,
323-
omega_num=omega_num)
324-
warnings.warn(
325-
"passing systems to `bode_plot` is deprecated; "
326-
"use `frequency_response()`", DeprecationWarning)
327-
if plot is None:
328-
plot = True # Keep track of legacy usage (see notes below)
329-
330318
#
331319
# Pre-process the data to be plotted (unwrap phase)
332320
#
@@ -337,6 +325,13 @@ def bode_plot(
337325
# the list of lines created, which is the new output for _plot functions.
338326
#
339327

328+
# If we were passed a list of systems, convert to data
329+
if all([isinstance(
330+
sys, (StateSpace, TransferFunction)) for sys in data]):
331+
data = frequency_response(
332+
data, omega=omega, omega_limits=omega_limits,
333+
omega_num=omega_num, Hz=Hz)
334+
340335
# If plot_phase is not specified, check the data first, otherwise true
341336
if plot_phase is None:
342337
plot_phase = True if data[0].plot_phase is None else data[0].plot_phase
@@ -409,28 +404,25 @@ def bode_plot(
409404
#
410405
# There are three possibilities at this stage in the code:
411406
#
412-
# * plot == True: either set explicitly by the user or we were passed a
413-
# non-FRD system instead of data. Return mag, phase, omega, with a
414-
# warning.
407+
# * plot == True: set explicitly by the user. Return mag, phase, omega,
408+
# with a warning.
415409
#
416410
# * plot == False: set explicitly by the user. Return mag, phase,
417411
# omega, with a warning.
418412
#
419-
# * plot == None: this is the new default setting and if it hasn't been
420-
# changed, then we use the v0.10+ standard of returning an array of
413+
# * plot == None: this is the new default setting. Return an array of
421414
# lines that were drawn.
422415
#
423-
# The one case that can cause problems is that a user called
424-
# `bode_plot` with an FRD system, didn't set the plot keyword
425-
# explicitly, and expected mag, phase, omega as a return value. This
426-
# is hopefully a rare case (it wasn't in any of our unit tests nor
427-
# examples at the time of v0.10.0).
416+
# If `bode_plot` was called with no `plot` argument and the return
417+
# values were used, the new code will cause problems (you get an array
418+
# of lines instead of magnitude, phase, and frequency). To recover the
419+
# old behavior, call `bode_plot` with `plot=True`.
428420
#
429421
# All of this should be removed in v0.11+ when we get rid of deprecated
430422
# code.
431423
#
432424

433-
if plot is True or plot is False:
425+
if plot is not None:
434426
warnings.warn(
435427
"`bode_plot` return values of mag, phase, omega is deprecated; "
436428
"use frequency_response()", DeprecationWarning)
@@ -1828,8 +1820,8 @@ def _compute_curve_offset(resp, mask, max_offset):
18281820
def gangof4_response(P, C, omega=None, Hz=False):
18291821
"""Compute the response of the "Gang of 4" transfer functions for a system.
18301822
1831-
Generates a 2x2 plot showing the "Gang of 4" sensitivity functions
1832-
[T, PS; CS, S].
1823+
Generates a 2x2 frequency response for the "Gang of 4" sensitivity
1824+
functions [T, PS; CS, S].
18331825
18341826
Parameters
18351827
----------
@@ -1840,7 +1832,9 @@ def gangof4_response(P, C, omega=None, Hz=False):
18401832
18411833
Returns
18421834
-------
1843-
None
1835+
response : :class:`~control.FrequencyResponseData`
1836+
Frequency response with inputs 'r' and 'd' and outputs 'y', and 'u'
1837+
representing the 2x2 matrix of transfer functions in the Gang of 4.
18441838
18451839
Examples
18461840
--------
@@ -1989,6 +1983,10 @@ def singular_values_plot(
19891983
Hz : bool
19901984
If True, plot frequency in Hz (omega must be provided in rad/sec).
19911985
Default value (False) set by config.defaults['freqplot.Hz'].
1986+
plot : bool, optional
1987+
(legacy) If given, `singular_values_plot` returns the legacy return
1988+
values of magnitude, phase, and frequency. If False, just return
1989+
the values with no plot.
19921990
*fmt : :func:`matplotlib.pyplot.plot` format string, optional
19931991
Passed to `matplotlib` as the format string for all lines in the plot.
19941992
The `omega` parameter must be present (use omega=None if needed).
@@ -2023,28 +2021,20 @@ def singular_values_plot(
20232021
freqplot_rcParams = config._get_param(
20242022
'freqplot', 'rcParams', kwargs, _freqplot_defaults, pop=True)
20252023

2026-
# Process legacy system arguments
2024+
# Convert systems into frequency responses
20272025
if any([isinstance(response, (StateSpace, TransferFunction))
20282026
for response in data]):
2029-
warnings.warn(
2030-
"passing systems to `singular_values_plot` is deprecated; "
2031-
"use `singular_values_response()`", DeprecationWarning)
20322027
responses = singular_values_response(
20332028
data, omega=omega, omega_limits=omega_limits,
20342029
omega_num=omega_num)
2035-
legacy_usage = True
20362030
else:
20372031
responses = data
2038-
legacy_usage = False
20392032

20402033
# Process (legacy) plot keyword
20412034
if plot is not None:
20422035
warnings.warn(
20432036
"`singular_values_plot` return values of sigma, omega is "
20442037
"deprecated; use singular_values_response()", DeprecationWarning)
2045-
legacy_usage = True
2046-
else:
2047-
plot = True
20482038

20492039
# Warn the user if we got past something that is not real-valued
20502040
if any([not np.allclose(np.imag(response.fresp[:, 0, :]), 0)
@@ -2172,7 +2162,8 @@ def singular_values_plot(
21722162
with plt.rc_context(freqplot_rcParams):
21732163
fig.suptitle(title)
21742164

2175-
if legacy_usage:
2165+
# Legacy return processing
2166+
if plot is not None:
21762167
if len(responses) == 1:
21772168
return sigmas[0], omegas[0]
21782169
else:

control/lti.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,8 +424,11 @@ def frequency_response(
424424
425425
Notes
426426
-----
427-
This function is a wrapper for :meth:`StateSpace.frequency_response` and
428-
:meth:`TransferFunction.frequency_response`.
427+
1. This function is a wrapper for :meth:`StateSpace.frequency_response`
428+
and :meth:`TransferFunction.frequency_response`.
429+
430+
2. You can also use the lower-level methods ``sys(s)`` or ``sys(z)`` to
431+
generate the frequency response for a single system.
429432
430433
Examples
431434
--------

control/matlab/wrappers.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,14 @@ def bode(*args, **kwargs):
6464
"""
6565
from ..freqplot import bode_plot
6666

67+
# Use the plot keyword to get legacy behavior
68+
# TODO: update to call frequency_response and then bode_plot
69+
kwargs = dict(kwargs) # make a copy since we modify this
70+
if 'plot' not in kwargs:
71+
kwargs['plot'] = True
72+
6773
# Turn off deprecation warning
6874
with warnings.catch_warnings():
69-
warnings.filterwarnings(
70-
'ignore', message='passing systems .* is deprecated',
71-
category=DeprecationWarning)
7275
warnings.filterwarnings(
7376
'ignore', message='.* return values of .* is deprecated',
7477
category=DeprecationWarning)

control/tests/config_test.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,36 +203,42 @@ def test_custom_bode_default(self, mplcleanup):
203203
@pytest.mark.usefixtures("legacy_plot_signature")
204204
def test_bode_number_of_samples(self, mplcleanup):
205205
# Set the number of samples (default is 50, from np.logspace)
206-
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87)
206+
mag_ret, phase_ret, omega_ret = ct.bode_plot(
207+
self.sys, omega_num=87, plot=True)
207208
assert len(mag_ret) == 87
208209

209210
# Change the default number of samples
210211
ct.config.defaults['freqplot.number_of_samples'] = 76
211-
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys)
212+
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, plot=True)
212213
assert len(mag_ret) == 76
213214

214215
# Override the default number of samples
215-
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87)
216+
mag_ret, phase_ret, omega_ret = ct.bode_plot(
217+
self.sys, omega_num=87, plot=True)
216218
assert len(mag_ret) == 87
217219

218220
@pytest.mark.usefixtures("legacy_plot_signature")
219221
def test_bode_feature_periphery_decade(self, mplcleanup):
220222
# Generate a sample Bode plot to figure out the range it uses
221223
ct.reset_defaults() # Make sure starting state is correct
222-
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=False)
224+
mag_ret, phase_ret, omega_ret = ct.bode_plot(
225+
self.sys, Hz=False, plot=True)
223226
omega_min, omega_max = omega_ret[[0, -1]]
224227

225228
# Reset the periphery decade value (should add one decade on each end)
226229
ct.config.defaults['freqplot.feature_periphery_decades'] = 2
227-
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=False)
230+
mag_ret, phase_ret, omega_ret = ct.bode_plot(
231+
self.sys, Hz=False, plot=True)
228232
np.testing.assert_almost_equal(omega_ret[0], omega_min/10)
229233
np.testing.assert_almost_equal(omega_ret[-1], omega_max * 10)
230234

231235
# Make sure it also works in rad/sec, in opposite direction
232-
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=True)
236+
mag_ret, phase_ret, omega_ret = ct.bode_plot(
237+
self.sys, Hz=True, plot=True)
233238
omega_min, omega_max = omega_ret[[0, -1]]
234239
ct.config.defaults['freqplot.feature_periphery_decades'] = 1
235-
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, Hz=True)
240+
mag_ret, phase_ret, omega_ret = ct.bode_plot(
241+
self.sys, Hz=True, plot=True)
236242
np.testing.assert_almost_equal(omega_ret[0], omega_min*10)
237243
np.testing.assert_almost_equal(omega_ret[-1], omega_max/10)
238244

control/tests/discrete_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ def test_discrete_bode(self, tsys):
465465
# Create a simple discrete time system and check the calculation
466466
sys = TransferFunction([1], [1, 0.5], 1)
467467
omega = [1, 2, 3]
468-
mag_out, phase_out, omega_out = bode(sys, omega)
468+
mag_out, phase_out, omega_out = bode(sys, omega, plot=True)
469469
H_z = list(map(lambda w: 1./(np.exp(1.j * w) + 0.5), omega))
470470
np.testing.assert_array_almost_equal(omega, omega_out)
471471
np.testing.assert_array_almost_equal(mag_out, np.absolute(H_z))

control/tests/freqresp_test.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,19 +360,20 @@ def test_options(editsdefaults):
360360
])
361361
def test_initial_phase(TF, initial_phase, default_phase, expected_phase):
362362
# Check initial phase of standard transfer functions
363-
mag, phase, omega = ctrl.bode(TF)
363+
mag, phase, omega = ctrl.bode(TF, plot=True)
364364
assert(abs(phase[0] - default_phase) < 0.1)
365365

366366
# Now reset the initial phase to +180 and see if things work
367-
mag, phase, omega = ctrl.bode(TF, initial_phase=initial_phase)
367+
mag, phase, omega = ctrl.bode(TF, initial_phase=initial_phase, plot=True)
368368
assert(abs(phase[0] - expected_phase) < 0.1)
369369

370370
# Make sure everything works in rad/sec as well
371371
if initial_phase:
372372
plt.xscale('linear') # avoids xlim warning on next line
373373
plt.clf() # clear previous figure (speeds things up)
374374
mag, phase, omega = ctrl.bode(
375-
TF, initial_phase=initial_phase/180. * math.pi, deg=False)
375+
TF, initial_phase=initial_phase/180. * math.pi,
376+
deg=False, plot=True)
376377
assert(abs(phase[0] - expected_phase) < 0.1)
377378

378379

@@ -399,7 +400,7 @@ def test_initial_phase(TF, initial_phase, default_phase, expected_phase):
399400
-270, -3*math.pi/2, math.pi/2, id="order5, -270"),
400401
])
401402
def test_phase_wrap(TF, wrap_phase, min_phase, max_phase):
402-
mag, phase, omega = ctrl.bode(TF, wrap_phase=wrap_phase)
403+
mag, phase, omega = ctrl.bode(TF, wrap_phase=wrap_phase, plot=True)
403404
assert(min(phase) >= min_phase)
404405
assert(max(phase) <= max_phase)
405406

0 commit comments

Comments
 (0)