New plotting paradigm · python-control/python-control · Discussion #645
@billtubbs posted in billtubbs/python-control-examples and mentioned in #65 (comment):
Plotting Paradigm
This is a proposal to introduce a different paradigm for the way specialised control analyses and plots are created in Python-Control.
It is based on the way plotting is done in Pandas.
For example:
s = pandas.Series(my_data, index=my_index, name="my series") # instantiate object print(s.dtype) # get an attribute s.plot(style="o-") # make a plot plt.show() avg = s.mean() # do something else
Functions this proposal could affect
root_locuspzmapbode_plotnyquist_plotgangof4_plotnichols_plotsisotool?
Rationale
The current paradigm for these plot functions is based on the way they are done in MATLAB using the functions pzmap, bode, nyquist, etc. These functions can produce a plot and/or the data itself, depending on how you use them. This style is typical of a functional programming language that does not have support for object-oriented programming.
Problem:
- Doing the calculations and making the plot are two separate tasks. Doing both with one function is arguably 'overloading' it. This leads to a larger set of arguments and return values and somewhat restricts the amount of variables that can be returned or accessed after the function is called.
Solution:
- Split the process into two steps:
- Step 1, do the calculations and generate the data
- Step 2, make the plot
- Use a Python object (class) to access the data and plot method.
The benefits of this approach are:
- The inputs, outputs and arguments of each step are separated
- A greater variety of variables and attributes can be accessed
- Easier to get a handle to the plot axis for customizing plots
- Easily extensible — more functionality could be added as methods
- It is familiar to many Python users (e.g. data scientists using Pandas).
To illustrate how this might work, I have outlined some examples below.
Example 1 – Root locus plot
Current method:
from control import rlocus rlocus(my_sys, **kwargs) plt.show()
Proposed method:
from control import RootLocus rl = RootLocus(my_sys, **kwargs) # calculation arguments go here rl.plot(**kwargs) # plotting arguments here plt.show()
or:
from control import RootLocus RootLocus(my_sys).plot(**kwargs) plt.show()
Calculate the root locus without making the plot
Current method:
rlist, klist = rlocus(my_sys, Plot=False)
Proposed method:
rl = RootLocus(my_sys) # does not make a plot by default rl.rlist, rl.klist # access the data
Calculate and plot the root locus
Current method:
rlist, klist = rlocus(my_sys) plt.show()
Proposed method:
rl = RootLocus(my_sys) rl.rlist, rl.klist # access the data rl.plot() plt.show()
Customizing the root locus plot
Current method:
rlocus(my_sys) ax = plt.gca() #ax.lines[??].set_linewidth(2) # not easy to do ax.annotate("label", (-1, 0)) plt.show()
Proposed method:
rl = RootLocus(my_sys) ax = rl.plot(linewidth=2) # plotting arguments here ax.annotate("label", (-1, 0)) plt.show()
Adding root locus to a custom plot
Current method:
fig, axes = plt.subplots(2, 1) ax = axes[0] rlocus(sys1, ax=axes[0]) ax.set_title("System 1") ax = axes[1] rlocus(sys2, ax=axes[1]) ax.set_title("System 2") plt.tight_layout() plt.show()
Proposed method:
rl1 = RootLocus(sys1) rl2 = RootLocus(sys2) fig, axes = plt.subplots(2, 1) ax = axes[0] rl1.plot(ax=ax) ax.set_title("System 1") ax = axes[1] rl2.plot(ax=ax) ax.set_title("System 2") plt.show()
Other comments
Introducing this would not require the replacement of the MATLAB-like functions rlocus, nyquist, pzmap, etc. although keeping the current root_locus, bode_plot, and nyquist_plot functions seems a bit redundant.
Functions such as freqresp would be unaffected and would probably be called by FreqResp to do the calculations.
The solution is not going to be as simple as the rlocus example above in the case of all plot functions. Some generate their own axes objects (e.g. radial plots) and some generate more than one axis (i.e. a complete figure), so each function will have to be considered case-by-case.