8000 gangof4 refactoring into response/plot + related bode_plot changes · controlPh/python-control@a7e9951 · GitHub
[go: up one dir, main page]

Skip to content

Commit a7e9951

Browse files
committed
gangof4 refactoring into response/plot + related bode_plot changes
1 parent c3ebbda commit a7e9951

File tree

12 files changed

+687
-336
lines changed

12 files changed

+687
-336
lines changed

control/frdata.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ def __init__(self, *args, **kwargs):
210210
# If data was generated by a system, keep track of that
211211
self.sysname = kwargs.pop('sysname', None)
212212

213+
# Keep track of default properties for plotting
214+
self.plot_phase=kwargs.pop('plot_phase', None)
215+
self.title=kwargs.pop('title', None)
216+
213217
# Keep track of return type
214218
self.return_magphase=kwargs.pop('return_magphase', False)
215219
if self.return_magphase not in (True, False):
@@ -650,7 +654,7 @@ def plot(self, *args, **kwargs):
650654

651655
# For now, only support Bode plots
652656
# TODO: add 'kind' keyword and Nyquist plots (?)
653-
bode_plot(self, *args, **kwargs)
657+
return bode_plot(self, *args, **kwargs)
654658

655659
# Convert to pandas
656660
def to_pandas(self):

control/freqplot.py

Lines changed: 434 additions & 317 deletions
Large diffs are not rendered by default.

control/matlab/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787

8888
# Functions that are renamed in MATLAB
8989
pole, zero = poles, zeros
90+
freqresp = frequency_response
9091

9192
# Import functions specific to Matlab compatibility package
9293
from .timeresp import *

control/matlab/wrappers.py

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

67-
# If first argument is a list, assume python-control calling format
68-
if hasattr(args[0], '__iter__'):
69-
return bode_plot(*args, **kwargs)
67+
# Turn off deprecation warning
68+
with warnings.catch_warnings():
69+
warnings.filterwarnings(
70+
'ignore', message='passing systems .* is deprecated',
71+
category=DeprecationWarning)
72+
warnings.filterwarnings(
73+
'ignore', message='.* return values of .* is deprecated',
74+
category=DeprecationWarning)
75+
76+
# If first argument is a list, assume python-control calling format
77+
if hasattr(args[0], '__iter__'):
78+
retval = bode_plot(*args, **kwargs)
79+
else:
80+
# Parse input arguments
81+
syslist, omega, args, other = _parse_freqplot_args(*args)
82+
kwargs.update(other)
7083

71-
# Parse input arguments
72-
syslist, omega, args, other = _parse_freqplot_args(*args)
73-
kwargs.update(other)
84+
# Call the bode command
85+
retval = bode_plot(syslist, omega, *args, **kwargs)
7486

75-
# Call the bode command
76-
return bode_plot(syslist, omega, *args, **kwargs)
87+
return retval
7788

7889

7990
def nyquist(*args, **kwargs):

control/tests/config_test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def test_default_deprecation(self):
8989
assert ct.config.defaults['bode.Hz'] \
9090
== ct.config.defaults['freqplot.Hz']
9191

92+
@pytest.mark.usefixtures("legacy_plot_signature")
9293
def test_fbs_bode(self, mplcleanup):
9394
ct.use_fbs_defaults()
9495

@@ -133,6 +134,7 @@ def test_fbs_bode(self, mplcleanup):
133134
phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data()
134135
np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2)
135136

137+
@pytest.mark.usefixtures("legacy_plot_signature")
136138
def test_matlab_bode(self, mplcleanup):
137139
ct.use_matlab_defaults()
138140

@@ -177,6 +179,7 @@ def test_matlab_bode(self, mplcleanup):
177179
phase_x, phase_y = (((plt.gcf().axes[1]).get_lines())[0]).get_data()
178180
np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2)
179181

182+
@pytest.mark.usefixtures("legacy_plot_signature")
180183
def test_custom_bode_default(self, mplcleanup):
181184
ct.config.defaults['freqplot.dB'] = True
182185
ct.config.defaults['freqplot.deg'] = True
@@ -198,6 +201,7 @@ def test_custom_bode_default(self, mplcleanup):
198201
np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3)
199202
np.testing.assert_almost_equal(phase_y[-1], -pi, decimal=2)
200203

204+
@pytest.mark.usefixtures("legacy_plot_signature")
201205
def test_bode_number_of_samples(self, mplcleanup):
202206
# Set the number of samples (default is 50, from np.logspace)
203207
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87)
@@ -212,6 +216,7 @@ def test_bode_number_of_samples(self, mplcleanup):
212216
mag_ret, phase_ret, omega_ret = ct.bode_plot(self.sys, omega_num=87)
213217
assert len(mag_ret) == 87
214218

219+
@pytest.mark.usefixtures("legacy_plot_signature")
215220
def test_bode_feature_periphery_decade(self, mplcleanup):
216221
# Generate a sample Bode plot to figure out the range it uses
217222
ct.reset_defaults() # Make sure starting state is correct

control/tests/conftest.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
import control
1111

12-
TEST_MATRIX_AND_ARRAY = os.getenv("PYTHON_CONTROL_ARRAY_AND_MATRIX") == "1"
13-
1412
# some common pytest marks. These can be used as test decorators or in
1513
# pytest.param(marks=)
1614
slycotonly = pytest.mark.skipif(
@@ -61,6 +59,20 @@ def mplcleanup():
6159
mpl.pyplot.close("all")
6260

6361

62+
@pytest.fixture(scope="function")
63+
def legacy_plot_signature():
64+
"""Turn off warnings for calls to plotting functions with old signatures"""
65+
import warnings
66+
warnings.filterwarnings(
67+
'ignore', message='passing systems .* is deprecated',
68+
category=DeprecationWarning)
69+
warnings.filterwarnings(
70+
'ignore', message='.* return values of .* is deprecated',
71+
category=DeprecationWarning)
72+
yield
73+
warnings.resetwarnings()
74+
75+
6476
# Allow pytest.mark.slow to mark slow tests (skip with pytest -m "not slow")
6577
def pytest_configure(config):
6678
config.addinivalue_line("markers", "slow: mark test as slow to run")

control/tests/convert_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def printSys(self, sys, ind):
4848
print("sys%i:\n" % ind)
4949
print(sys)
5050

51+
@pytest.mark.usefixtures("legacy_plot_signature")
5152
@pytest.mark.parametrize("states", range(1, maxStates))
5253
@pytest.mark.parametrize("inputs", range(1, maxIO))
5354
@pytest.mark.parametrize("outputs", range(1, maxIO))

control/tests/discrete_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ def test_sample_tf(self, tsys):
460460
np.testing.assert_array_almost_equal(numd, numd_expected)
461461
np.testing.assert_array_almost_equal(dend, dend_expected)
462462

463+
@pytest.mark.usefixtures("legacy_plot_signature")
463464
def test_discrete_bode(self, tsys):
464465
# Create a simple discrete time system and check the calculation
465466
sys = TransferFunction([1], [1, 0.5], 1)

control/tests/freqplot_test.py

Lines changed: 166 additions & 1 deletion
< 663D tr class="diff-line-row">
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,134 @@
1010
from control.tests.conftest import slycotonly
1111
pytestmark = pytest.mark.usefixtures("mplcleanup")
1212

13+
#
14+
# Define a system for testing out different sharing options
15+
#
16+
17+
omega = np.logspace(-2, 2, 5)
18+
fresp1 = np.array([10 + 0j, 5 - 5j, 1 - 1j, 0.5 - 1j, -.1j])
19+
fresp2 = np.array([1j, 0.5 - 0.5j, -0.5, 0.1 - 0.1j, -.05j]) * 0.1
20+
fresp3 = np.array([10 + 0j, -20j, -10, 2j, 1])
21+
fresp4 = np.array([10 + 0j, 5 - 5j, 1 - 1j, 0.5 - 1j, -.1j]) * 0.01
22+
23+
fresp = np.empty((2, 2, omega.size), dtype=complex)
24+
fresp[0, 0] = fresp1
25+
fresp[0, 1] = fresp2
26+
fresp[1, 0] = fresp3
27+
fresp[1, 1] = fresp4
28+
manual_response = ct.FrequencyResponseData(
29+
fresp, omega, sysname="Manual Response")
30+
31+
@pytest.mark.parametrize(
32+
"sys", [
33+
ct.tf([1], [1, 2, 1], name='System 1'), # SISO
34+
manual_response, # simple MIMO
35+
])
36+
# @pytest.mark.parametrize("pltmag", [True, False])
37+
# @pytest.mark.parametrize("pltphs", [True, False])
38+
# @pytest.mark.parametrize("shrmag", ['row', 'all', False, None])
39+
# @pytest.mark.parametrize("shrphs", ['row', 'all', False, None])
40+
# @pytest.mark.parametrize("shrfrq", ['col', 'all', False, None])
41+
# @pytest.mark.parametrize("secsys", [False, True])
42+
@pytest.mark.parametrize( # combinatorial-style test (faster)
43+
"pltmag, pltphs, shrmag, shrphs, shrfrq, secsys",
44+
[(True, True, None, None, None, False),
45+
(True, False, None, None, None, False),
46+
(False, True, None, None, None, False),
47+
(True, True, None, None, None, True),
48+
(True, True, 'row', 'row', 'col', False),
49+
(True, True, 'row', 'row', 'all', True),
50+
(True, True, 'all', 'row', None, False),
51+
(True, True, 'row', 'all', None, True),
52+
(True, True, 'none', 'none', None, True),
53+
(True, False, 'all', 'row', None, False),
54+
(True, True, True, 'row', None, True),
55+
(True, True, None, 'row', True, False),
56+
(True, True, 'row', None, None, True),
57+
])
58+
def test_response_plots(
59+
sys, pltmag, pltphs, shrmag, shrphs, shrfrq, secsys, clear=True):
60+
61+
# Save up the keyword arguments
62+
kwargs = dict(
63+
plot_magnitude=pltmag, plot_phase=pltphs,
64+
share_magnitude=shrmag, share_phase=shrphs, share_frequency=shrfrq,
65+
# overlay_outputs=ovlout, overlay_inputs=ovlinp
66+
)
67+
68+
# Create the response
69+
if isinstance(sys, ct.FrequencyResponseData):
70+
response = sys
71+
else:
72+
response = ct.frequency_response(sys)
73+
74+
# Look for cases where there are no data to plot
75+
if not pltmag and not pltphs:
76+
return None
77+
78+
# Plot the frequency response
79+
plt.figure()
80+
out = response.plot(**kwargs)
81+
82+
# Make sure all of the outputs are of the right type
83+
nlines_plotted = 0
84+
for ax_lines in np.nditer(out, flags=["refs_ok"]):
85+
for line in ax_lines.item():
86+
assert isinstance(line, mpl.lines.Line2D)
87+
nlines_plotted += 1
88+
89+
# Make sure number of plots is correct
90+
nlines_expected = response.ninputs * response.noutputs * \
91+
(2 if pltmag and pltphs else 1)
92+
assert nlines_plotted == nlines_expected
93+
94+
# Save the old axes to compare later
95+
old_axes = plt.gcf().get_axes()
96+
97+
# Add additional data (and provide info in the title)
98+
if secsys:
99+
newsys = ct.rss(
100+
4, sys.noutputs, sys.ninputs, strictly_proper=True)
101+
ct.frequency_response(newsys).plot(**kwargs)
102+
103+
# Make sure we have the same axes
104+
new_axes = plt.gcf().get_axes()
105+
assert new_axes == old_axes
106+
107+
# Make sure every axes has multiple lines
108+
for ax in new_axes:
109+
assert len(ax.get_lines()) > 1
110+
111+
# Update the title so we can see what is going on
112+
fig = out[0, 0][0].axes.figure
113+
fig.suptitle(
114+
fig._suptitle._text +
115+
f" [{sys.noutputs}x{sys.ninputs}, pm={pltmag}, pp={pltphs},"
116+
f" sm={shrmag}, sp={shrphs}, sf={shrfrq}]", # TODO: ", "
117+
# f"oo={ovlout}, oi={ovlinp}, ss={secsys}]", # TODO: add back
118+
fontsize='small')
119+
120+
# Get rid of the figure to free up memory
121+
if clear:
122+
plt.close('.Figure')
123+
124+
125+
# Use the manaul response to verify that different settings are working
126+
def test_manual_response_limits():
127+
# Default response: limits should be the same across rows
128+
out = manual_response.plot()
129+
axs = ct.get_plot_axes(out)
130+
for i in range(manual_response.noutputs):
131+
for j in range(1, manual_response.ninputs):
132+
# Everything in the same row should have the same limits
133+
assert axs[i*2, 0].get_ylim() == axs[i*2, j].get_ylim()
134+
assert axs[i*2 + 1, 0].get_ylim() == axs[i*2 + 1, j].get_ylim()
135+
# Different rows have different limits
136+
assert axs[0, 0].get_ylim() != axs[2, 0].get_ylim()
137+
assert axs[1, 0].get_ylim() != axs[3, 0].get_ylim()
138+
139+
# TODO: finish writing tests
140+
13141
def test_basic_freq_plots(savefigs=False):
14142
# Basic SISO Bode plot
15143
plt.figure()
@@ -23,7 +151,7 @@ def test_basic_freq_plots(savefigs=False):
23151

24152
# Basic MIMO Bode plot
25153
plt.figure()
26-
sys_mimo = ct.tf2ss(
154+
sys_mimo = ct.tf(
27155
[[[1], [0.1]], [[0.2], [1]]],
28156
[[[1, 0.6, 1], [1, 1, 1]], [[1, 0.4, 1], [1, 2, 1]]], name="MIMO")
29157
ct.frequency_response(sys_mimo).plot()
@@ -41,6 +169,17 @@ def test_basic_freq_plots(savefigs=False):
41169
ct.frequency_response(sys_mimo).plot(plot_magnitude=False)
42170

43171

172+
def test_gangof4_plots(savefigs=False):
173+
proc = ct.tf([1], [1, 1, 1], name="process")
174+
ctrl = ct.tf([100], [1, 5], name="control")
175+
176+
plt.figure()
177+
ct.gangof4_plot(proc, ctrl)
178+
179+
if savefigs:
180+
plt.savefig('freqplot-gangof4.png')
181+
182+
44183
if __name__ == "__main__":
45184
#
46185
# Interactive mode: generate plots for manual viewing
@@ -55,10 +194,36 @@ def test_basic_freq_plots(savefigs=False):
55194
# Start by clearing existing figures
56195
plt.close('all')
57196

197+
# Define a set of systems to test
198+
sys_siso = ct.tf([1], [1, 2, 1], name="SISO")
199+
sys_mimo = ct.tf(
200+
[[[1], [0.1]], [[0.2], [1]]],
201+
[[[1, 0.6, 1], [1, 1, 1]], [[1, 0.4, 1], [1, 2, 1]]], name="MIMO")
202+
sys_test = manual_response
203+
204+
# Run through a large number of test cases
205+
test_cases = [
206+
# sys pltmag pltphs shrmag shrphs shrfrq secsys
207+
(sys_siso, True, True, None, None, None, False),
208+
(sys_siso, True, True, None, None, None, True),
209+
(sys_mimo, True, True, 'row', 'row', 'col', False),
210+
(sys_mimo, True, True, 'row', 'row', 'col', True),
211+
(sys_test, True, True, 'row', 'row', 'col', False),
212+
(sys_test, True, True, 'row', 'row', 'col', True),
213+
(sys_test, True, True, 'none', 'none', 'col', True),
214+
(sys_test, True, True, 'all', 'row', 'col', False),
215+
(sys_test, True, True, 'row', 'all', 'col', True),
216+
(sys_test, True, True, None, 'row', 'col', False),
217+
(sys_test, True, True, 'row', None, 'col', True),
218+
]
219+
for args in test_cases:
220+
test_response_plots(*args, clear=False)
221+
58222
# Define and run a selected set of interesting tests
59223
# TODO: TBD (see timeplot_test.py for format)
60224

61225
test_basic_freq_plots(savefigs=True)
226+
test_gangof4_plots(savefigs=True)
62227

63228
#
64229
# Run a few more special cases to show off capabilities (and save some

0 commit comments

Comments
 (0)
0