Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
43e3779
DEV:
forgi86 Mar 27, 2021
ae7d546
Update control/freqplot.py
forgi86 Mar 28, 2021
36e8b77
Update control/freqplot.py
forgi86 Mar 28, 2021
4e055bb
FIX:
forgi86 Mar 28, 2021
14ef194
Merge remote-tracking branch 'origin/singular-values-plot' into singu…
forgi86 Mar 28, 2021
681edb7
FIX:
forgi86 Mar 28, 2021
8d02fae
FIX:
forgi86 Mar 28, 2021
fe97069
FIX:
forgi86 Mar 28, 2021
762fbd6
FIX: development of parametrized tests for the singular_values_plot f…
forgi86 Mar 31, 2021
3b09ae7
FIX: using option squeeze=False to keep all dimensions from the frequ…
forgi86 Mar 31, 2021
9c68700
FIX:
forgi86 Mar 31, 2021
a392c97
DOC:
forgi86 Mar 31, 2021
caa3e2c
FIX:
forgi86 Mar 31, 2021
0607314
FIX:
forgi86 Mar 31, 2021
65b4e49
TST:
forgi86 Mar 31, 2021
87d48ab
TST:
forgi86 Apr 1, 2021
a289f8c
MAINT: refactoring of the frequency selection logic as a private func…
forgi86 Apr 1, 2021
fbafc6c
FIX: added missing omega vector in test system ss_siso_ct
forgi86 Apr 2, 2021
7f5d66c
TST:
forgi86 Apr 2, 2021
81f9ee7
TST: added a test condition that expects the warning to be issued whe…
forgi86 Apr 2, 2021
7d19b30
MAINT:
forgi86 Apr 3, 2021
34c3826
DOC: updated example for singular_values_plot
forgi86 Apr 7, 2021
48015d5
MAINT: _bode_defaults removed, fields moved in _freqplot_default
forgi86 Apr 7, 2021
38dfeda
MAINT: no warning issued for evaluation above the Nyquist frequency
forgi86 Apr 7, 2021
555902b
MAINT: removing references to singular_values_plot configurations
forgi86 Apr 7, 2021
ec69337
MAINT: removing references to singular_values_plot configurations
forgi86 Apr 7, 2021
288a7ee
MAINT: removing references to bode configurations (use freqplot instead)
forgi86 Apr 7, 2021
8f3f76d
Update control/freqplot.py
forgi86 Apr 7, 2021
fb3d9a0
MAINT: the Hz parameter of bode_plot and singular_values_plot now onl…
forgi86 Apr 18, 2021
37c0962
DOC: bode changed to freqplot in conventions.rst
forgi86 Apr 18, 2021
a486c18
Merge remote-tracking branch 'origin/singular-values-plot' into singu…
forgi86 Apr 18, 2021
9143d75
Merge branch 'master' into singular-values-plot
forgi86 Apr 18, 2021
f5de343
DOC: modified docstring (Examples section) in singular_values_plot to…
forgi86 Apr 23, 2021
9638e92
DOC: modified docstring (Examples section) in singular_values_plot to…
forgi86 Apr 23, 2021
10cde40
DOC: modified docstring (Examples section) in singular_values_plot to…
forgi86 Apr 23, 2021
0ada3f6
DOC: modified imports in docstring (Examples section) in singular_val…
forgi86 Apr 24, 2021
649c394
Merge branch 'master' into pr-593
bnavigator Apr 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 162 additions & 2 deletions control/freqplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@
from .exception import ControlMIMONotImplemented
from .statesp import StateSpace
from .xferfcn import TransferFunction
from .lti import evalfr
Comment thread
bnavigator marked this conversation as resolved.
Resolved
from . import config

__all__ = ['bode_plot', 'nyquist_plot', 'gangof4_plot',
__all__ = ['bode_plot', 'nyquist_plot', 'gangof4_plot', 'singular_values_plot',
'bode', 'nyquist', 'gangof4']

# Default values for module parameter variables
Expand Down Expand Up @@ -172,7 +173,7 @@ def bode_plot(syslist, omega=None,
>>> mag, phase, omega = bode(sys)

"""
# Make a copy of the kwargs dictonary since we will modify it
# Make a copy of the kwargs dictionary since we will modify it
kwargs = dict(kwargs)

# Check to see if legacy 'Plot' keyword was used
Expand Down Expand Up @@ -1039,14 +1040,173 @@ def gangof4_plot(P, C, omega=None, **kwargs):

plt.tight_layout()

#
# Singular value plot
#


def singular_values_plot(syslist, omega=None,
plot=True, omega_limits=None, omega_num=None,
*args, **kwargs):
"""Singular value plot for a system

Plots a Singular Value plot for the system over a (optional) frequency range.

Parameters
----------
syslist : linsys
List of linear systems (single system is OK)
omega : array_like
List of frequencies in rad/sec to be used for frequency response
plot : bool
If True (default), plot magnitude and phase
Comment thread
bnavigator marked this conversation as resolved.
Resolved
omega_limits : array_like of two values
Limits of the to generate frequency vector.
Comment thread
forgi86 marked this conversation as resolved.
Resolved
If Hz=True the limits are in Hz otherwise in rad/s.
omega_num : int
Number of samples to plot. Defaults to
config.defaults['freqplot.number_of_samples'].

Returns
-------
sigma : ndarray (or list of ndarray if len(syslist) > 1))
singular values
omega : ndarray (or list of ndarray if len(syslist) > 1))
frequency in rad/sec

Other Parameters
----------------
grid : bool
If True, plot grid lines on gain and phase plots. Default is set by
`config.defaults['bode.grid']`.
Comment thread
bnavigator marked this conversation as resolved.
Resolved

Examples
--------
>>> den = [75, 1]
>>> sys = ct.tf([[[87.8], [-86.4]], [[108.2], [-109.6]]], [[den, den], [den, den]])
>>> omega = np.logspace(-4, 1, 1000)
>>> sigma, omega = singular_values_plot(sys)
Comment thread
bnavigator marked this conversation as resolved.
Resolved

"""
# Make a copy of the kwargs dictionary since we will modify it
kwargs = dict(kwargs)

# Check to see if legacy 'Plot' keyword was used
if 'Plot' in kwargs:
import warnings
warnings.warn("'Plot' keyword is deprecated in bode_plot; use 'plot'",
FutureWarning)
# Map 'Plot' keyword to 'plot' keyword
plot = kwargs.pop('Plot')
Comment thread
bnavigator marked this conversation as resolved.
Resolved

# Get values for params (and pop from list to allow keyword use in plot)
dB = config._get_param('bode', 'dB', kwargs, _bode_defaults, pop=True)
Hz = config._get_param('bode', 'Hz', kwargs, _bode_defaults, pop=True)
grid = config._get_param('bode', 'grid', kwargs, _bode_defaults, pop=True)
plot = config._get_param('bode', 'grid', plot, True)
Comment thread
bnavigator marked this conversation as resolved.
Resolved

# If argument was a singleton, turn it into a tuple
if not hasattr(syslist, '__iter__'):
syslist = (syslist,)

# Decide whether to go above Nyquist frequency
omega_range_given = True if omega is not None else False

if omega is None:
omega_num = config._get_param(
'freqplot', 'number_of_samples', omega_num)
if omega_limits is None:
# Select a default range if none is provided
omega = _default_frequency_range(syslist, number_of_samples=omega_num)
else:
omega_range_given = True
omega_limits = np.asarray(omega_limits)
if len(omega_limits) != 2:
raise ValueError("len(omega_limits) must be 2")
if Hz:
omega_limits *= 2. * math.pi
omega = np.logspace(np.log10(omega_limits[0]),
np.log10(omega_limits[1]), num=omega_num,
endpoint=True)
Comment thread
bnavigator marked this conversation as resolved.
Resolved

if plot:
fig = plt.gcf()
ax_sigma = None

# Get the current axes if they already exist
for ax in fig.axes:
if ax.get_label() == 'control-sigma':
ax_sigma = ax

# If no axes present, create them from scratch
if ax_sigma is None:
plt.clf()
ax_sigma = plt.subplot(111, label='control-sigma')

color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']

sigmas, omegas, nyquistfrqs = [], [], []
for idx_sys, sys in enumerate(syslist):
omega_sys = np.asarray(omega)
if sys.isdtime(strict=True):
nyquistfrq = math.pi / sys.dt
if not omega_range_given:
Comment thread
bnavigator marked this conversation as resolved.
Resolved
# limit up to and including nyquist frequency
omega_sys = np.hstack((
omega_sys[omega_sys < nyquistfrq], nyquistfrq))
else:
nyquistfrq = None

mag, phase, omega = sys.frequency_response(omega)
fresp = mag * np.exp(1j * phase)
#fresp = evalfr(sys, 1j * omega_sys)
Comment thread
forgi86 marked this conversation as resolved.
Resolved

fresp = fresp.transpose((2, 0, 1))
sigma = np.linalg.svd(fresp, compute_uv=False)

sigmas.append(sigma)
omegas.append(omega_sys)
nyquistfrqs.append(nyquistfrq)

if plot:
color = color_cycle[idx_sys % len(color_cycle)]

nyquistfrq_plot = None
if Hz:
omega_plot = omega_sys / (2. * math.pi)
if nyquistfrq:
nyquistfrq_plot = nyquistfrq / (2. * math.pi)
else:
omega_plot = omega_sys
if nyquistfrq:
nyquistfrq_plot = nyquistfrq
sigma_plot = sigma

if dB:
ax_sigma.semilogx(omega_plot, 20 * np.log10(sigma_plot), color=color, *args, **kwargs)
else:
ax_sigma.loglog(omega_plot, sigma_plot, color=color, *args, **kwargs)

if nyquistfrq_plot is not None:
ax_sigma.axvline(x=nyquistfrq_plot, color=color)

# Add a grid to the plot + labeling
ax_sigma.grid(grid, which='both')
ax_sigma.set_ylabel("Magnitude (dB)" if dB else "Magnitude")
ax_sigma.set_xlabel("Frequency (Hz)" if Hz else "Frequency (rad/sec)")

if len(syslist) == 1:
return sigmas[0], omegas[0]
else:
return sigmas, omegas
#
# Utility functions
#
# This section of the code contains some utility functions for
# generating frequency domain plots
#


# Compute reasonable defaults for axes
def _default_frequency_range(syslist, Hz=None, number_of_samples=None,
feature_periphery_decades=None):
Expand Down
17 changes: 16 additions & 1 deletion control/tests/freqresp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from control.statesp import StateSpace
from control.xferfcn import TransferFunction
from control.matlab import ss, tf, bode, rss
from control.freqplot import bode_plot, nyquist_plot
from control.freqplot import bode_plot, nyquist_plot, singular_values_plot
from control.tests.conftest import slycotonly

pytestmark = pytest.mark.usefixtures("mplcleanup")
Expand All @@ -39,6 +39,7 @@ def ss_mimo():
D = np.array([[0, 0]])
return StateSpace(A, B, C, D)


def test_freqresp_siso(ss_siso):
"""Test SISO frequency response"""
omega = np.linspace(10e-2, 10e2, 1000)
Expand Down Expand Up @@ -508,3 +509,17 @@ def test_dcgain_consistency():

sys_ss = ctrl.tf2ss(sys_tf)
np.testing.assert_almost_equal(sys_ss.dcgain(), -1)


def test_singular_values_plot():
Comment thread
bnavigator marked this conversation as resolved.
Resolved
den = [75, 1]
sys = tf([[[87.8], [-86.4]],
[[108.2], [-109.6]]],
[[den, den],
[den, den]])
sigma, omega = singular_values_plot(sys, 0.0)
sys_dc = np.array([[87.8, -86.4],
[108.2, -109.6]])

u, s, v = np.linalg.svd(sys_dc)
Comment thread
bnavigator marked this conversation as resolved.
Resolved
np.testing.assert_almost_equal(sigma.ravel(), s.ravel())
Loading