Skip to content

Commit f2c40e6

Browse files
committed
Added handling of bode_plot frequency limits for discrete systems.
1 parent c2c7381 commit f2c40e6

3 files changed

Lines changed: 50 additions & 28 deletions

File tree

control/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
bode_dB = False # Bode plot magnitude units
1212
bode_deg = True # Bode Plot phase units
1313
bode_Hz = False # Bode plot frequency units
14+
bode_number_of_samples = None # Bode plot number of samples
15+
bode_feature_periphery_decade = 1.0 # Bode plot feature periphery in decades
1416

1517
# Set defaults to match MATLAB
1618
def use_matlab_defaults():

control/freqplot.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444
import matplotlib.pyplot as plt
4545
import scipy as sp
4646
import numpy as np
47-
from warnings import warn
47+
#from warnings import warn
4848
from .ctrlutil import unwrap
4949
from .bdalg import feedback
50-
from .lti import isdtime, timebaseEqual
50+
#from .lti import isdtime, timebaseEqual
5151

5252
__all__ = ['bode_plot', 'nyquist_plot', 'gangof4_plot',
5353
'bode', 'nyquist', 'gangof4']
@@ -122,12 +122,12 @@ def bode_plot(syslist, omega=None, omega_num=None, dB=None, Hz=None, deg=None,
122122

123123
if omega is None:
124124
# Select a default range if none is provided
125-
omega = default_frequency_range(syslist)
125+
omega = default_frequency_range(syslist, Hz=Hz)
126126
elif (isinstance(omega, tuple) or isinstance(omega, list)) and len(omega) == 2:
127127
if omega_num:
128-
omega = sp.logspace(np.log10(omega[0]), np.log10(omega[1]), num=omega_num, endpoint=False)
128+
omega = sp.logspace(np.log10(omega[0]), np.log10(omega[1]), num=omega_num, endpoint=True)
129129
else:
130-
omega = sp.logspace(np.log10(omega[0]), np.log10(omega[1]), endpoint=False)
130+
omega = sp.logspace(np.log10(omega[0]), np.log10(omega[1]), endpoint=True)
131131

132132
mags, phases, omegas, nyquistfrqs = [], [], [], []
133133
for sys in syslist:
@@ -138,7 +138,8 @@ def bode_plot(syslist, omega=None, omega_num=None, dB=None, Hz=None, deg=None,
138138
omega_sys = np.array(omega)
139139
if sys.isdtime(True):
140140
nyquistfrq = 2. * np.pi * 1./sys.dt / 2.
141-
omega_sys = omega_sys[omega_sys < nyquistfrq]
141+
nyquistfrq_limit = nyquistfrq - (omega_sys[-1] - omega_sys[-2]) /2. # floating point!
142+
omega_sys = omega_sys[omega_sys < nyquistfrq_limit]
142143
else:
143144
nyquistfrq = None
144145
# Get the magnitude and phase of the system
@@ -153,6 +154,8 @@ def bode_plot(syslist, omega=None, omega_num=None, dB=None, Hz=None, deg=None,
153154
nyquistfrq_plot = nyquistfrq / (2. * np.pi)
154155
else:
155156
omega_plot = omega_sys
157+
if nyquistfrq:
158+
nyquistfrq_plot = nyquistfrq
156159

157160
mags.append(mag)
158161
phases.append(phase)
@@ -359,7 +362,7 @@ def gangof4_plot(P, C, omega=None):
359362
#
360363

361364
# Compute reasonable defaults for axes
362-
def default_frequency_range(syslist, number_of_samples=None):
365+
def default_frequency_range(syslist, Hz=None, number_of_samples=None, feature_periphery_decade=None):
363366
"""Compute a reasonable default frequency range for frequency
364367
domain plots.
365368
@@ -390,6 +393,13 @@ def default_frequency_range(syslist, number_of_samples=None):
390393
# integer. It excludes poles and zeros at the origin. If no features
391394
# are found, it turns logspace(-1, 1)
392395

396+
# Set default values for options
397+
from . import config
398+
if (number_of_samples is None):
399+
number_of_samples = config.bode_number_of_samples
400+
if (feature_periphery_decade is None):
401+
feature_periphery_decade = config.bode_feature_periphery_decade
402+
393403
# Find the list of all poles and zeros in the systems
394404
features = np.array(())
395405
freq_interesting = []
@@ -402,42 +412,54 @@ def default_frequency_range(syslist, number_of_samples=None):
402412
try:
403413
# Add new features to the list
404414
if sys.isctime():
405-
features = np.concatenate((features, np.abs(sys.pole())))
406-
features = np.concatenate((features, np.abs(sys.zero())))
415+
features_ = np.abs(sys.pole())
416+
features_ = np.concatenate((features_, np.abs(sys.zero())))
417+
# Get rid of poles and zeros at the origin
418+
features_ = features_[features_ != 0.0];
419+
features = np.concatenate((features, features_))
407420
elif sys.isdtime(strict=True):
408421
freq_interesting.append(np.pi*1./sys.dt)
409-
# TODO: how to determine lowest interesting frequency?
422+
p = sys.pole()
423+
p = p[p != -1.]
424+
features = np.concatenate((features, np.abs(np.log(p)/sys.dt)))
425+
z = sys.zero()
426+
z = z[(z.imag != 0.0)]
427+
#z = z[(z.imag != 0.0) | (z.real > 0.0)]
428+
features = np.concatenate((features, np.abs(np.log(z)/sys.dt)))
410429
else:
411430
pass
412431
# TODO:
413432
except:
414433
pass
415434

416-
# Get rid of poles and zeros at the origin
417-
features = features[features != 0];
418435

419436
# Make sure there is at least one point in the range
420-
if (features.shape[0] == 0): features = [1];
421-
437+
if (features.shape[0] == 0):
438+
features = [1];
439+
440+
if Hz:
441+
features /= 2.*np.pi
442+
features = np.log10(features)
443+
lsp_min = np.floor(np.min(features)) - feature_periphery_decade
444+
lsp_max = np.ceil(np.max(features)) + feature_periphery_decade
445+
lsp_min += np.log10(2.*np.pi)
446+
lsp_max += np.log10(2.*np.pi)
447+
else:
422448
# Take the log of the features
423-
# TODO: in case of Hz==True: wouldn't it make more sense to to set the limits to decades in Hz instead of rad/s?
424-
features = np.log10(features)
425-
lsp_min = np.floor(np.min(features))-1
426-
lsp_max = np.ceil(np.max(features))+1
449+
features = np.log10(features)
450+
lsp_min = np.floor(np.min(features)) - feature_periphery_decade
451+
lsp_max = np.ceil(np.max(features)) + feature_periphery_decade
427452
if freq_interesting:
428453
lsp_min = min(lsp_min, np.log10(min(freq_interesting)))
429454
lsp_max = max(lsp_max, np.log10(max(freq_interesting)))
430455

431-
#! TODO: Add a check in discrete case to make sure we don't get aliasing
456+
#! TODO: Add a check in discrete case to make sure we don't get aliasing (Attention: there is a list of system but only one omega vector)
432457

433458
# Set the range to be an order of magnitude beyond any features
434459
if number_of_samples:
435-
omega = sp.logspace(lsp_min, lsp_max, num=number_of_samples, endpoint=False)
460+
omega = sp.logspace(lsp_min, lsp_max, num=number_of_samples, endpoint=True)
436461
else:
437-
omega = sp.logspace(lsp_min, lsp_max, endpoint=False)
438-
439-
440-
462+
omega = sp.logspace(lsp_min, lsp_max, endpoint=True)
441463
return omega
442464

443465
#

control/tests/bode_plot__manualtest.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
'''
2-
Created on 3 juli 2015
3-
4-
@author: marho40
1+
#!/usr/bin/env python
2+
'''bode_plot__manualtest.py
53
'''
64

75
from __future__ import print_function

0 commit comments

Comments
 (0)