Skip to content

Commit 6de5e91

Browse files
committed
update MATLAB interface to bode, nyquist
* regularize argument parsing by using _parse_freqplot_args * fix bug in exception handling (ControlArgument not defined) * change printed warning to use warnings.warn * add unit tests for exceptions, warnings
1 parent 1888854 commit 6de5e91

3 files changed

Lines changed: 102 additions & 18 deletions

File tree

control/matlab/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070

7171
# Import MATLAB-like functions that can be used as-is
7272
from ..ctrlutil import *
73-
from ..freqplot import nyquist, gangof4
73+
from ..freqplot import gangof4
7474
from ..nichols import nichols
7575
from ..bdalg import *
7676
from ..pzmap import *

control/matlab/wrappers.py

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
"""
2-
Wrappers for the Matlab compatibility module
2+
Wrappers for the MATLAB compatibility module
33
"""
44

55
import numpy as np
66
from ..statesp import ss
77
from ..xferfcn import tf
8+
from ..ctrlutil import issys
9+
from ..exception import ControlArgument
810
from scipy.signal import zpk2tf
11+
from warnings import warn
912

10-
__all__ = ['bode', 'ngrid', 'dcgain']
13+
__all__ = ['bode', 'nyquist', 'ngrid', 'dcgain']
1114

12-
def bode(*args, **keywords):
15+
def bode(*args, **kwargs):
1316
"""bode(syslist[, omega, dB, Hz, deg, ...])
1417
1518
Bode plot of the frequency response
@@ -36,7 +39,7 @@ def bode(*args, **keywords):
3639
If True, plot frequency in Hz (omega must be provided in rad/sec)
3740
deg : boolean
3841
If True, return phase in degrees (else radians)
39-
Plot : boolean
42+
plot : boolean
4043
If True, plot magnitude and phase
4144
4245
Examples
@@ -53,19 +56,65 @@ def bode(*args, **keywords):
5356
* >>> bode(sys1, sys2, ..., sysN, w)
5457
* >>> bode(sys1, 'plotstyle1', ..., sysN, 'plotstyleN')
5558
"""
59+
from ..freqplot import bode_plot
5660

57-
# If the first argument is a list, then assume python-control calling format
58-
from ..freqplot import bode as bode_orig
61+
# If first argument is a list, assume python-control calling format
5962
if (getattr(args[0], '__iter__', False)):
60-
return bode_orig(*args, **keywords)
63+
return bode_plot(*args, **kwargs)
6164

62-
# Otherwise, run through the arguments and collect up arguments
63-
syslist = []; plotstyle=[]; omega=None;
65+
# Parse input arguments
66+
syslist, omega, args, other = _parse_freqplot_args(*args)
67+
kwargs.update(other)
68+
69+
# Call the bode command
70+
return bode_plot(syslist, omega, *args, **kwargs)
71+
72+
73+
def nyquist(*args, **kwargs):
74+
"""nyquist(syslist[, omega])
75+
76+
Nyquist plot of the frequency response
77+
78+
Plots a Nyquist plot for the system over a (optional) frequency range.
79+
80+
Parameters
81+
----------
82+
sys1, ..., sysn : list of LTI
83+
List of linear input/output systems (single system is OK).
84+
omega : array_like
85+
Set of frequencies to be evaluated, in rad/sec.
86+
87+
Returns
88+
-------
89+
real : ndarray (or list of ndarray if len(syslist) > 1))
90+
real part of the frequency response array
91+
imag : ndarray (or list of ndarray if len(syslist) > 1))
92+
imaginary part of the frequency response array
93+
omega : ndarray (or list of ndarray if len(syslist) > 1))
94+
frequencies in rad/s
95+
96+
"""
97+
from ..freqplot import nyquist_plot
98+
99+
# If first argument is a list, assume python-control calling format
100+
if (getattr(args[0], '__iter__', False)):
101+
return nyquist_plot(*args, **kwargs)
102+
103+
# Parse arguments
104+
syslist, omega, args, other = _parse_freqplot_args(*args)
105+
kwargs.update(other)
106+
107+
# Call the nyquist command
108+
return nyquist_plot(syslist, omega, *args, **kwargs)
109+
110+
111+
def _parse_freqplot_args(*args):
112+
"""Parse arguments to frequency plot routines (bode, nyquist)"""
113+
syslist, plotstyle, omega, other = [], [], None, {}
64114
i = 0;
65115
while i < len(args):
66116
# Check to see if this is a system of some sort
67-
from ..ctrlutil import issys
68-
if (issys(args[i])):
117+
if issys(args[i]):
69118
# Append the system to our list of systems
70119
syslist.append(args[i])
71120
i += 1
@@ -79,11 +128,16 @@ def bode(*args, **keywords):
79128
continue
80129

81130
# See if this is a frequency list
82-
elif (isinstance(args[i], (list, np.ndarray))):
131+
elif isinstance(args[i], (list, np.ndarray)):
83132
omega = args[i]
84133
i += 1
85134
break
86135

136+
# See if this is a frequency range
137+
elif isinstance(args[i], tuple) and len(args[i]) == 2:
138+
other['omega_limits'] = args[i]
139+
i += 1
140+
87141
else:
88142
raise ControlArgument("unrecognized argument type")
89143

@@ -93,22 +147,30 @@ def bode(*args, **keywords):
93147

94148
# Check to make sure we got the same number of plotstyles as systems
95149
if (len(plotstyle) != 0 and len(syslist) != len(plotstyle)):
96-
raise ControlArgument("number of systems and plotstyles should be equal")
150+
raise ControlArgument(
151+
"number of systems and plotstyles should be equal")
97152

98153
# Warn about unimplemented plotstyles
99154
#! TODO: remove this when plot styles are implemented in bode()
100155
#! TODO: uncomment unit test code that tests this out
101156
if (len(plotstyle) != 0):
102-
print("Warning (matlab.bode): plot styles not implemented");
157+
warn("Warning (matlab.bode): plot styles not implemented");
158+
159+
if len(syslist) == 0:
160+
raise ControlArgument("no systems specified")
161+
elif len(syslist) == 1:
162+
# If only one system given, retun just that system (not a list)
163+
syslist = syslist[0]
164+
165+
return syslist, omega, plotstyle, other
103166

104-
# Call the bode command
105-
return bode_orig(syslist, omega, **keywords)
106167

107168
from ..nichols import nichols_grid
108169
def ngrid():
109170
return nichols_grid()
110171
ngrid.__doc__ = nichols_grid.__doc__
111172

173+
112174
def dcgain(*args):
113175
'''
114176
Compute the gain of the system in steady state.

control/tests/matlab_test.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from control.matlab import pade
2828
from control.matlab import unwrap, c2d, isctime, isdtime
2929
from control.matlab import connect, append
30-
30+
from control.exception import ControlArgument
3131

3232
from control.frdata import FRD
3333
from control.tests.conftest import slycotonly
@@ -802,6 +802,28 @@ def test_tf_string_args(self):
802802
np.testing.assert_array_almost_equal(G.den, [[[1, 2, 1]]])
803803
assert isdtime(G, strict=True)
804804

805+
def test_matlab_wrapper_exceptions(self):
806+
"""Test out exceptions in matlab/wrappers.py"""
807+
sys = tf([1], [1, 2, 1])
808+
809+
# Extra arguments in bode
810+
with pytest.raises(ControlArgument, match="not all arguments"):
811+
bode(sys, 'r-', [1e-2, 1e2], 5.0)
812+
813+
# Multiple plot styles
814+
with pytest.warns(UserWarning, match="plot styles not implemented"):
815+
bode(sys, 'r-', sys, 'b--', [1e-2, 1e2])
816+
817+
# Incorrect number of arguments to dcgain
818+
with pytest.raises(ValueError, match="needs either 1, 2, 3 or 4"):
819+
dcgain(1, 2, 3, 4, 5)
820+
821+
def test_matlab_freqplot_passthru(self):
822+
"""Test nyquist and bode to make sure the pass arguments through"""
823+
sys = tf([1], [1, 2, 1])
824+
bode((sys,)) # Passing tuple will call bode_plot
825+
nyquist((sys,)) # Passing tuple will call nyquist_plot
826+
805827

806828
#! TODO: not yet implemented
807829
# def testMIMOtfdata(self):

0 commit comments

Comments
 (0)