Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

New plotting paradigm#645

bnavigator started this conversation inIdeas
Aug 10, 2021· 22 comments· 58 replies
Discussion options

@billtubbs posted inbilltubbs/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 inPandas.

For example:

s=pandas.Series(my_data,index=my_index,name="my series")# instantiate objectprint(s.dtype)# get an attributes.plot(style="o-")# make a plotplt.show()avg=s.mean()# do something else

Functions this proposal could affect

  • root_locus
  • pzmap
  • bode_plot
  • nyquist_plot
  • gangof4_plot
  • nichols_plot
  • sisotool?

Rationale

The current paradigm for these plot functions is based on the way they are done in MATLAB using the functionspzmap,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:

fromcontrolimportrlocusrlocus(my_sys,**kwargs)plt.show()

Proposed method:

fromcontrolimportRootLocusrl=RootLocus(my_sys,**kwargs)# calculation arguments go hererl.plot(**kwargs)# plotting arguments hereplt.show()

or:

fromcontrolimportRootLocusRootLocus(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 defaultrl.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 datarl.plot()plt.show()

Customizing the root locus plot

Current method:

rlocus(my_sys)ax=plt.gca()#ax.lines[??].set_linewidth(2)  # not easy to doax.annotate("label", (-1,0))plt.show()

Proposed method:

rl=RootLocus(my_sys)ax=rl.plot(linewidth=2)# plotting arguments hereax.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 functionsrlocus,nyquist,pzmap, etc. although keeping the currentroot_locus,bode_plot, andnyquist_plot functions seems a bit redundant.

Functions such asfreqresp would be unaffected and would probably be called byFreqResp 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.

You must be logged in to vote

Replies: 22 comments 58 replies

Comment options

bnavigator
Aug 10, 2021
Collaborator Author

I like it very much.

We can keep theroot_locus,bode_plot, andnyquist_plot functions by having them call the appropriate calls to the new API internally and deprecate them for removal after 2 or 3 minor version updates. (Or major version updates if we decide to cut version 1.0 eventually)

The MATLAB wrappers could to the same without deprecation.

You must be logged in to vote
0 replies
Comment options

For reference, I found the old discussion we were having about this at the back end of#291. I think the main problems encountered back then are now fixed (e.g. functionssgrid andzgrid callingplt.figure), but it's worth a quick read as there are probably some other complexities encountered then that we will have to address when we get into the weeds on this again.

You must be logged in to vote
0 replies
Comment options

I also like this proposal quite a lot.

We might think through what we want to do from time response and how this fits it. For example, the various time response functions (forced_response,input_output_response,step_response, etc) could return aTimeSeries object with data elementstime,input,output,state (ort,u,y,x?) and we could have aplot method.

Another thing to think about: what happens if you want to combine several plots, either in an array (a laplt.subplots) or as multiple lines on a single set of axes? Just call theplot method multiple times?

You must be logged in to vote
1 reply
@billtubbs
Comment options

The question about time responses and time series data is a good one. I've been thinking about it too. We don't want to make Python-Control dependent on Pandas but I would argue that the PandasSeries andDataFrame objects are perfectly-designed tools for time series data storage, plotting and analysis (I use them all the time when working with industrial time series data).

If we can find a way to seamlessly use Python-Control and Pandas 'hand-in-glove' so-to-speak, that would be very powerful I think. (This could also save us writing a raft of tools to do all that).

Comment options

Found another example of this paradigm from scipy.spatialhere:

importmatplotlib.pyplotaspltimportnumpyasnpfromscipy.spatialimportConvexHull,convex_hull_plot_2dpoints=np.random.random((30,2))hull=ConvexHull(points)_=convex_hull_plot_2d(hull)plt.show()

Here, they use a separate functionconvex_hull_plot_2d for the plotting step and it returns a hook to the whole figure. Not sure why they didn't giveConvexHull aplot_2d() method.
Same thing forscipy.spatial.Voronoi andscipy.spatial.voronoi_plot_2d.

You must be logged in to vote
0 replies
Comment options

I am definitely in favor of separating plots and computation.

Just curious about the advantage to@billtubbs suggestion of creating new types of objects, perhaps built on or inspired by pandas, that include a plot method. I think that would require creation of a series of new classes to hold root locus info, frequency response info, and time response info, to name the new classes I can think of that would be required.

Is there a reason it wouldn't be enough to use the simpler alternative of finishing separating out dual-purpose functions likerlocus into computation, e.g.root_locus and plotting, e.g.root_locus_plot? *

importcontrolasctfig,axes=plt.subplots(2,1)ax=axes[0]ct.root_locus_plot(ax=ax)ax.set_title("System 1")ax=axes[1]ct.step_plot(ax=ax)ax.set_title("System 2")```*outsideofthematlablayer,frequencyresponseplotslargelyexist:``nyquist_plot``,``bode_plot``,``nichols_plot``,buttimeresponseplotsdon'tseemto,e.g.``step_plot``,``impulse_plot``
You must be logged in to vote
2 replies
@billtubbs
Comment options

Thanks, I agree, we need to have a good justification to create any new data objects. One thing with two functions, you need to transfer the data from the calculation function to the plotting function. If it's just a couple of things, like 2 arrays, then I think a tuple or dict is fine and it doesn't merit creating an object.

A benefit of an object would be if there is additional 'meta data' that could be automatically passed to the plotting method, such as axis and legend labels. As an example,df.plot() (with no additional arguments), wheredf is a Pandas dataframe, makes a complete plot with multiple coloured lines, a legend with the names of each series and an x-axis label all from the metadata in the dataframe object.

The case for a new data object gets stronger if it also has additional uses in its own right (i.e. for things other than simply making a plot). I was also wondering, if we make oneFreqResp object, can it be used to make a Bode, Nyquist, and Nichols chart? Is that possible? Then, does theFreqResp object have any other uses beyond making plots? In other words, could it be useful in other analysis workflows?

So, in summary I think we need to really challenge the need for any new object and only do it if it has enough benefits over a simpler solution (like two separate functions).

@sawyerbfuller
Comment options

The case for a new data object gets stronger if it also has additional uses in its own right (i.e. for things other than simply making a plot). I was also wondering, if we make one FreqResp object, can it be used to make a Bode, Nyquist, and Nichols chart? Is that possible? Then, does the FreqResp object have any other uses beyond making plots? In other words, could it be useful in other analysis workflows?

I think there already is an object that can storefrequency_response data:frd.FrequencyResponseData. It can also be used for system-id applications. (By the way,freqresp is now deprecated and only part of matlab layer).

I think there is a case to be made that aTimeResponseData (OrTimeSeriesData) object, with an optionalinput field as suggested by@murrayrm could have a similar use.

Then this proposal could include simply adding a plot method to these, and maybe adding one more object forroot_locus?

Comment options

@murrayrm , in response to your other point:

Another thing to think about: what happens if you want to combine several plots, either in an array (a laplt.subplots) or as multiple lines on a single set of axes? Just call theplot method multiple times?

I made a quick demo of how this can be done in Pandas:
https://github.com/billtubbs/python-control-examples/blob/main/Demonstrate-Pandas-time-series-plotting.ipynb

You must be logged in to vote
0 replies
Comment options

FYI, there is a similar concept that has been implemented inJuliaControl PR #529, where they return a result structure for time responses.

You must be logged in to vote
5 replies
@billtubbs
Comment options

It also says this at the bottom:

I will model the freqresp return structure similarly in a new PR when this has been merged

@billtubbs
Comment options

Had a quick look at theJulia proposal. Looks like functions likelsim,step, etc. will in future return aSimResult(y, t, x, uout, sys) struct (Julia doesn't have object-oriented programming, i.e. classes) and this struct can then be used as a single argument in a new method of the plot function, so you can do:

sim_result=lsim(my_sys, u, t)plot(sim_result)

or simply:

plot(lsim(my_sys, u, t))

For comparison, here is what this could look like in Python Control:

sim_result=input_output_response(my_sys,u,t)sim_result.plot()

or

input_output_response(my_sys,u,t).plot()
@billtubbs
Comment options

Perhaps, we could preserve the old usage of returning a tuple for the matlab-like functionslsim,step, etc:

yout,t,xout=lsim(sys,u,t,x0)
@murrayrm
Comment options

I've been playing around with some ideas for time responses and I have an implementation that allows the following to work:

response = ct.input_output_response(sys, t, u)response.plot()

or you can say

t, y = ct.input_output_response(sys, t, u)plt.plot(t, y)

This is all done by defining a new classInputOutputResponse and then changing the time response functions to return an element of that class. By implementing an__iter__ method in the class, you can allow the object to be "assigned" to a tuple.

So far, everything is backward compatible so that all unit tests run without change. But we get the advantage of having an object returned to which we can add additional methods (likeplot).

I'll do a bit more work on it and post a draft PR so that people can take a look. I think it fits the intent of the paradigm here.

@billtubbs
Comment options

For the input-output response, it would be nice (IMO) ifresponse.plot() produced a figure with two subplots: upper plot of (t, y), lower plot of (t, u), with appropriate titles ('Outputs', 'Inputs') and legends ('y_1', 'y_2', ...).

Comment options

Interesting proposal. During the pandemic, I've developed a simple plotting lib on the top of python control for my students. The lib uses the Plotly backend (which is also supported now by Panda) since my students mainly use the web-based interface Jupyter for running python scripts

You must be logged in to vote
0 replies
Comment options

Here is a summary of the relevant analysis and plotting functions currently inJulia ControlSystems (v0.8.0).

FunctionPurpose
bodeCompute the magnitude and phase of the frequency response
bodeplotCreate a Bode plot of the system
freqrespEvaluate the frequency response of a linear system
gangoffourplotGang-of-Four plot
marginCompute frequencies for gain margins, gain margins, frequencies for phase margins, phase margins
marginplotPlot all the amplitude and phase margins of the system
nicholsplotCreate a Nichols plot of the system
nyquistCompute the real and imaginary parts of the frequency response
nyquistplotCreate a Nyquist plot of the system
pzmapCreate a pole-zero map of the system
rlocusComputes and plots the root locus of the SISO LTISystem (deprecated)
rlocusplotComputes and plots the root locus of the SISO LTISystem
sigmaCompute the singular values sv of the frequency response
sigmaplotPlot the singular values of the frequency response of the system

Looks like the pattern is to make separate functions for calculations and plotting where applicable.

Is there any reason why we didn't name the Python-Controlroot_locus functionroot_locus_plot? All our other plot functions are clearly differentiable from their calculation-only equivalents.

You must be logged in to vote
0 replies
Comment options

Here is a table showing the correspondence between current Julia ControlSystems and Python-Control plot functions and the changes I am proposing to

  • separate calculation-only functions from plotting functions,
  • make our plot functions more consistent with Julia,
  • implement this new paradigm where applicable.

Can some of you please go through these and provide comments and feedback?

JuliaPython-Control currentProposal
mag, phase, w = bode(sys)mag, phase, w = bode_plot(sys, plot=False)fr = freqresp(sys)
fr = freqresp(sys, w)mag, phase, w = control.matlab.freqresp(sys, w)fr = freqresp(sys, w)
n/an/afr.plot()
bodeplot(sys)bode_plot(sys)no change
fig = bodeplot(sys)n/afig, axes = bode_plot(sys)
gangoffourplot(P, C)gangof4_plot(P, C)no change
fig = gangoffourplot(P, C)n/afig, axes = gangof4_plot(P, C)
ωgm, gm, ωpm, pm = margin(sys)gm, pm, wg, wp = margin(sys)no change
marginplot(sys)n/ano change for now
nicholsplot(sys)nichols_plot(sys)no change
fig = nicholsplot(sys)n/aax = nichols_plot(sys)
n/an/anichols_plot(sys, ax=ax)
re, im, w = nyquist(sys)n/ano change for now
nyquistplot(sys)nyquist_plot(sys)no change
fig = nyquistplot(sys)n/aax = nyquist_plot(sys)
n/an/anyquist_plot(sys, ax=ax)
p, z = pole(sys), tzero(sys)p, z = pzmap(sys, plot=False)p, z = pole(sys), zero(sys)
pzmap(sys)pzmap(sys)pole_zero_map(sys)
fig = pzmap(fig, sys)n/aax = pole_zero_map(sys)
n/an/apole_zero_map(sys, ax=ax)
n/aroots, k_out = root_locus(sys, plot=None)roots, k_out = root_locus(sys)
rlocusplot(sys)root_locus(sys)root_locus_plot(sys)
n/an/aax = root_locus_plot(sys)
n/an/aroot_locus_plot(sys, ax=ax)
sv, w = sigma(sys)sigma, w = singular_values_plot(sys, plot=False)sigma, w = singular_values(sys)
sigmaplot(sys)singular_values_plot(sys)no change
n/an/aax = singular_values_plot(sys)
n/an/asingular_values_plot(ax=ax)
y, t, x = step(sys)T, yout = step_response(sys)response = step_response(sys)*
y, t, x = impulse(sys)T, yout = impulse_response(sys)response = impulse_response(sys)*
y, t, x = lsim(sys, u, t)T, yout = forced_response(sys, T, u)response = forced_response(sys, T, u)*
Plots.plot(t, y)plt.plot(T, yout)response.plot()*
n/an/aax = response.plot()*
n/an/aresponse.plot(ax=ax)*

* - these changes are part of a different pull request by Richard.

You must be logged in to vote
0 replies
Comment options

Hi@billtubbs,

  • an alternative tofreqresp(sys, w) is the more pythonicmag, phase, w = sys.frequency_response(w) Remarks: 1. It occurs to me there should probably also be a functionfrequency_response(sys,w). 2. A todo is to change its output to be simply a complex array egx = sys.frequency_response(w) as in matlab ).
  • also new and not fully documented is the more concise complex frequency response:x = sys(s)
  • there is alsostability_margin That returns the stability margin in addition to gain and phase margins. I have also often wanted amargin_plot Function.
  • I think there might now be asingular_values_plot Function.
  • there are alreadypole Andzero Functions so that is already done.
You must be logged in to vote
2 replies
@billtubbs
Comment options

Thanks for the feedback@sawyerbfuller.

an alternative to freqresp(sys, w) is the more pythonic mag, phase, w = sys.frequency_response(w)

I agree that providing the most commonly-used functions as methods ofsys is a good idea. The question is where to draw the line. Here are some other things that could be done (which doesn't mean they should be!):

  • sys.frequency_response(w)
  • sys.margin()
  • sys.pzmap()
  • sys.root_locus()
  • sys.bode_plot()
  • sys.nyquist_plot()
  • ...etc.

Or we could have some kind of catch-all plot method (similar to Pandas):

  • sys.plot(kind="nyquist")

or

  • sys.plot.nyquist()

I don't plan to do anything with thesys object at the moment unless others think it's important. We can always add these later once the basic plotting functions are sorted out.

@billtubbs
Comment options

I think there might now be asingular_values_plot Function.

I see that now. I'll add this to the table above. Thanks.

Comment options

A few comments:

  • Regardingroot_locus versusroot_locus_plot: I don't think there was a specific reason we did this. Using_plot for anything that generates a plot makes sense as a pattern (similar to Julia, but using our underscore convention).

  • Another function to think about ispzmap: should this bepole_zero_plot? Or perhapspole_zero_map? (Note that we can always setpzmap = pole_zero_plot to allow the old usage and we should make sure that the MATLAB compatibility module maintains MATLAB names and signatures. I likepole_zero_plot in terms of consistency, but pzmap should probably still work.

  • I like the idea of having all plotting routines return an axis (or tuple of axes forbode_plot?).

  • I like the idea that all "response" functions return an object and you can then call theplot method to get something useful, with thekind keyword to generate some standard variants (e.g.,bode,nichols,nyquist for frequency responses, perhapsoutput-only,input-output,output-input for time responses?).

  • I likefrequency_response overfreqresp, especially since we deprecated the latter inREBASE: use __call__ instead of evalfr in lti system classes #499.

  • Perhaps we should (eventually) allow eitherct.method(sys) orsys.method() to work, similar to NumPy. Callingct.method(sys) would then just call the appropriate method.

  • For thepzmap() function: perhaps this should accept a tuple as its input argument (in addition to a system). So you could saypole_zero_plot((poles, zeros)) orpole_zero_plot(sys).

You must be logged in to vote
8 replies
@sawyerbfuller
Comment options

@billtubbs i am also in favor of everything suggested by@murrayrm above.

In regards to thefrd Class, it would be nice if we could re-use it to hold data coming out of egfrequency_response, but it does a lot of other things eg can be multiplied by a transfer function. Maybe it would become ungainly?

@billtubbs in regards tofrequency_response, my recollection was that there was consensus that it was an accident that it outputsmag, phase, w. It should change to output response as complex number. If you wantmag, phase, w, maybe you could use the new plot-freebode command.

In regards to bode vs. nyquist plots, it turns out they both kind of want similar frequency points( eg both need lots of frequency points around resonant peaks) so it might be possible to do as you suggest and use the same data for each. This is how it works now.

@murrayrm
Comment options

For the return value forsys.frequency_response, you can set things up so that this is aFrequencyResponseData object and then set up an__iter__ method that returns either mag, phase, omega (default) or s, omega. Similarly, you can use theproperty decorator to allow you to have attributes that convert data types. So something like the following should be possible:

fr = sys.frequency_response()d, w = fr.d, fr.w                         # d is complex, w is realmag, phase, w = fr                        # implement via __iter__mag, phase, w = fr.mag, fr.phase, fr.w    # implement via @propertyd, w = fr(return_complex=True)            # implement via __iter__fr.plot(kind='bode')ct.bode_plot(sys)                         # same plotct.bode_plot(fr)                          # same plotfr.plot(kind='nyquist')ct.nyquist_plot(sys)                      # same plotct.nyquist_plot(fr)                       # same plot

Doing things this way will make everything backward compatible with the current code but also alow us to be consistent with the new plotting paradigm. (This is also compatible with the way thatTimeResponseData works and you can look there for an example of how to implement.)

Regarding Bode vs Nyquist: there are two differences that I can think of that will need some thought:

  • For Nyquist plots, we want to capture the value of the transfer function at (and near) s = 0, but for Bode plots this point is not shown, so should be omitted when plotting.
  • For Nyquist plots, we add indentations around poles on the imaginary axis. These aren't really part of the frequency response, and so we would need to think through how they would be represented in theFrequencyResponseData object (perhaps a separate attribute?).

Finally, the current code fornyquist_plot returns the number of encirclements. If we change this to return handles to the figure and axis, we should think about how/where to return the number of encirclements.

@billtubbs
Comment options

Thanks for the feedback. This looks like a good path forward. Quick question:

d, w = fr(return_complex=True)            # implement via __iter__

Why not

d, w = sys.frequency_response(return_complex=True)d, w = frequency_response(syslist, return_complex=True)
@billtubbs
Comment options

Oh, I see thatTimeResponseData uses this approach withreturn_x for backwards compatibility. Okay, not a bad idea.

@murrayrm
Comment options

I agree that those variants should also work. The main thing is that the__iter__ method needs to know how many (and what type) of values to return. So we can keep the default as mag, phase, w for backward compatibility, but implement other options as well (with keywords).

Comment options

I see now that MATLAB has afrequency response data model object which can be legitimately used to store FR data simply for analysis:

>>loadAnalyzerData>>sys= frd(resp,freq);>>syssys=     Frequency(rad/s)Response           ----------------            --------0.10002.384e-01-1.951e-03i0.10272.540e-01-3.511e-03i0.10562.430e-01+1.807e-03i0.10852.573e-01-4.672e-03i0.11142.525e-01-7.747e-03i...

sys is an frd model object, which is a data container for representing frequency response data.
You can use frd models with many frequency-domain analysis commands. For example, visualize the frequency response data using bode.

You must be logged in to vote
6 replies
@billtubbs
Comment options

They allow bode, nyquist and nichols plots on FRD sys objects. I tested them and they look okay with the example data above.

For a system with imaginary poles I'm not sure. I tried this:

>>sys= tf(1, [12*0.21]);>> [re,im,wout]= nyquist(sys);>>fr_data= squeeze(re)+ squeeze(im)*i;>>fr_sys= frd(fr_data,wout);>> subplot(1,2,1)>> nyquist(sys)>> subplot(1,2,2)>> nyquist(fr_sys)

and the two plots are identical. But when I generated some random FRD data from this system and tried to make the nyquist plot it had to think for a very long time and the resulting plot looked odd. Is this what you meant?

>>fr_data2= squeeze(re)+0.01*randn(size(re))+ (squeeze(im)+0.01*randn(size(im)))*i;>>fr_sys2= frd(fr_data2,wout);
@billtubbs
Comment options

Oh wait, you said pure imaginary. Sorry.
I tested this system:

>>sys= tf(1, conv([-0.5i1],[0.5i1]));>> figure(1)>> subplot(1,2,1)>> nyquist(sys)>> [re,im,wout]= nyquist(sys);>>fr_data= squeeze(re)+ squeeze(im)*i;>>fr_sys= frd(fr_data,wout);>> subplot(1,2,2)>> nyquist(fr_sys)

and get the following:
nyquist_2opi

@billtubbs
Comment options

Ah, note the scaling on the axes above (10^-8, 10^7). When you re-scale the MATLAB nyquist plot it looks like this:
nyquist_2opi_ml

Here is Python control Nyquist plot with same axis scaling:
nyquist_2opi_py

What is going on here?

@bnavigator
Comment options

bnavigatorJan 4, 2022
Collaborator Author

That's the infinite circle feature (#534), see also#671

@billtubbs
Comment options

Ah right. Neat. To represent "to infinity and back" is it?

Comment options

I just noticed this comment inbode_plot in freqplot.py:

# Get the dimensions of the current axis, which we will divide up
# TODO: Not current implemented; just use subplot for now

This hadn't occurred to me actually—that you could take an existing subplot axis and divide it into two to make the mag and phase plots. As discussed, my plan was to return a complete new figure and axes:

fig,axes=bode_plot(sys)

Are there scenarios where we would want to embed a complete (mag + phase) Bode plot inside an existing figure?

I think it might be simpler to provide the ability to make individualbode_mag andbode_phase plots (returning theax only) so the user can construct whatever they want with those.

You must be logged in to vote
1 reply
@billtubbs
Comment options

How about this solution:

bode_mag_plot(syslist)# quick plotbode_mag_plot(syslist,ax=ax)# adding to existing figureax=bode_mag_plot(syslist)# returning axis handle

and same forbode_phase_plot()

And then in the new paradigm:

fr=frequency_response(syslist)# quick plotfr.plot(kind='bode')fr.plot(kind='bode_mag')fr.plot(kind='bode_phase')# orfig,axes=fr.plot(kind='bode')fr.plot(kind='bode_mag',ax=ax)ax=fr.plot(kind='bode_phase')...etc
Comment options

Can anyone think of a good name for this function to provide the calculations we are proposing to remove fromnyquist_plot? Does this even make sense? Do we need the contours and counts other than in the context of having made a plot? What are they used for?

counts,contours=________(syslist,omega=None,omega_limits=None,omega_num=None,warn_nyquist=True,*args,**kwargs)

Docstring:

    """Nyquist countour and number of encirclements for one or more systems.    Calculates the Nyquist contour over a (optional) frequency range and the    number of encirclements of the critical point (-1, 0). The curve is computed by     evaluating the Nyqist segment along the positive imaginary axis, with a     mirror image generated to reflect the negative imaginary axis.  Poles on     or near the imaginary axis are avoided using a small indentation.  The     portion of the Nyquist contour at infinity is not explicitly computed     (since it maps to a constant value for any system with a proper transfer    function).    Parameters    ----------    syslist : list of LTI        List of linear input/output systems (single system is OK). Nyquist        curves for each system are plotted on the same graph.    omega : array_like        Set of frequencies to be evaluated, in rad/sec.    omega_limits : array_like of two values        Limits to the range of frequencies. Ignored if omega is provided, and        auto-generated if omitted.    omega_num : int        Number of frequency samples to plot.  Defaults to        config.defaults['freqplot.number_of_samples'].    indent_radius : float        Amount to indent the Nyquist contour around poles that are at or near        the imaginary axis.    indent_direction : str        For poles on the imaginary axis, set the direction of indentation to        be 'right' (default), 'left', or 'none'.    warn_nyquist : bool, optional        If set to 'False', turn off warnings about frequencies above Nyquist.    Returns    -------    count : int (or list of int if len(syslist) > 1)        Number of encirclements of the point -1 by the Nyquist curve.  If        multiple systems are given, an array of counts is returned.    contour : ndarray (or list of ndarray if len(syslist) > 1)), optional        The contour used to create the primary Nyquist curve segment.  To        obtain the Nyquist curve values, evaluate system(s) along contour.    """

I'm not entirely sure we will need to keep this function when the new plotting paradigm is complete. Then we only need to returncounts andcontours as attributes (properties) of theFrequencyResponseData object. Still, we might need better names for them. "counts" is a bit vague when out of context, but something likenumber_of_encirclements is obviously way too much of a mouthful.nyquist_counts,nyquist_contours would be the simplest.

Other thoughts:n_circs

Or we could return a dictionary callednyquist_criterion,nyquist_stability_criteria or something, and put the two arguments inside this with appropriate names.

(I checked my French control course notes and there it is "le nombre de tours" which is much more concise!)

You must be logged in to vote
1 reply
@murrayrm
Comment options

Some ideas:

  • count[, contour] = nyquist_analysis(sys, return_contour=True/False)
  • count[, contour] = nyquist_criterion(sys, return_contour=True/False)
  • contour[, count] = nyquist_curve(sys, return_count=True/False)

Also, we should keep in mind that for a MIMO system, the Nyquist criterion is different (one looks at net encirclements of the origin for$\det(I + L(s))$. This isn't currently implemented, but we should make sure that we don't get in the way of that.

Comment options

Progress update on my work on this so far. I'm about 75% through step 1 right now.@murrayrm would you prefer me to submit a pull request now or at the end (it passes all the tests including new ones I added). Or I can wait until State 1 is complete:

Stage 1 - separate plotting and frequency analysis calculations into separate functions

  • New Bode plot function:fig, axes = bode_plot(syslist, ax=ax, ...)- complete
  • New (possibly temporary)frequency_response_bode(syslist, ...) function (equivalent to the previousmag, phase, omega = bode_plot(syslist, plot=False))- complete
  • New Nyquist plot function:ax = nyquist_plot(sys, ax=ax)- complete
  • New (possibly temporary)counts, contours = nyquist_stability_criterion(syslist, ...) function- complete
  • New gangof4_plot function:fig, axes = gangof4_plot(sys, axes=axes)- complete
  • Newax = pole_zero_plot(sys, ax=ax) function- complete
  • Newax = root_locus_plot(sys, ax=ax) function
  • Newax = nichols_plot(sys, ax=ax) function
  • Newax = singular_values_plot(sys) function- complete
  • Newsigma, w = singular_values(sys) function.- complete

Stage 2 - Introduce new plotting paradigm for frequency response

  • Add new properties toFrequencyResponseData object (mag,phase,nyquist_analysis, etc.)
  • Modifyfrequency_response function to returnFrequencyResponseData object
  • Addplot methods toFrequencyResponseData object
  • Possibly make apole_zero_plot method for LTI systems.
  • Possibly change filenames and re-organize a bit e.g.freqplot.py ->freqresp.py
You must be logged in to vote
0 replies
Comment options

@murrayrm would you prefer me to submit a pull request now or at the end

If you want comments along the way, post as a draft PR and we can take a look at the code and post thoughts. Otherwise, fine to wait until you have something working. Don't forget unit tests (coverage should not go down) and doc/ updates (adding functions, perhaps a new section describing the basic framework).

You must be logged in to vote
5 replies
@billtubbs
Comment options

How do I post as a draft PR? Just give you a link to my fork? All the unit tests have been updated so it's a working branch.

@billtubbs
Comment options

Yes I need to write the documentation. And I am working on a Jupyter notebook as a tutorial to all the plotting commands.

@murrayrm
Comment options

Like a normal PR, but there will be a point in the process where you can mark it as draft. Once it is there, everyone can see and comment, but can't merge. As you update your branch on GitHub, the PR gets updated.

@billtubbs
Comment options

Sounds good. I should probably merge my branch with my master first I guess, and then submit my master for the draft pull request.

@murrayrm
Comment options

It's usually better to work in a branch and then submit the PR from the branch (in your fork). Just make sure to build (or rebase) on top of the latest (upstream) master.

Comment options

Just to confirm. I'm finding code like this in some of the current plotting functions, E.g. fromsingular_values_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')

As I understand it, what this is doing is getting the current figure from pyplot and looking for an axes with the right label and plotting the output to that.

Under the changes here, there is the option to pass the axes you want to plot into as an argument and so none of this axes-labelling and searching is needed.

Or am I missing something?

(I know sisotool and gangof4 plots also use this but I think these functions will be able to use the new approach using the axes argument).

You must be logged in to vote
5 replies
@murrayrm
Comment options

For this case (singular_values_plot) I think it can probably be removed. But for something like a Bode plot, there are two axes => you need labels (I think) in order to get the magnitude and phase axes (subplots).

More generally, think the question is what we want the behavior to be if you do something like this:

ct.bode_plot(sys1)ct.bode_plot(sys2)

versus something like this:

ct.bode_plot(sys1)ct.nyquist_plot(sys1)

I think the behavior in the first case should be to plot the second system on the same axes as the first. But for the second case it seems like we should get a new set of axes (probably on the same figure, clearing the old one?).

@billtubbs
Comment options

I recommend we follow the conventions of Matplotlib and Pandas and not try to make our plot functions too 'clever'.

The convention when plotting in pyplot mode is that it will always try to add the new plot to whatever is in the current axis, regardless of what that is or if it makes sense. The only time a plot function creates a new figure is when none exists.

I checked in Pandas and it does indeed try to add anything to the current plot, even when this makes no sense:
Screen Shot 2022-01-09 at 19 22 32

This is in fact what happens in my current branch:
Screen Shot 2022-01-09 at 19 28 41

When something like this happens, the user should think "oh, I should have opened a new figure before making the second plot" which should lead them to the solution (which is more explicit):

sys = tf(1, [1, 2, 1])ct.bode_plot(sys)plt.figure()ct.nyquist_plot(sys)

I get why it was being handled differently before (with a hidden labelling system) to replicate MATLAB behaviour but to me that's not a fool-proof method and it's a bit confusing for the user who may not know what determines whether a plot is added to an existing axes or when it is erased first or when a new figure is created.

The nice thing about the changes we're introducing now is that if they want, the user can take full control of where the plot goes using the new arguments and returned objects:

fig,axes=plt.subplots(2,2)ct.bode_plot(sys,axes=axes[:,0])ct.nyquist_plot(sys,ax=axes[0,1])ct.step_response(sys,ax=axes[1,1])plt.show()

But I'm happy to be out-voted on this one and re-introduce the 'smart' replacement of any existing plots/lines.

@billtubbs
Comment options

B.t.w. I can't see any reason why you would want to clear any existing content in an axis or figure and replace it. In matplotlib, plots aren't shown until the end (unlike Matlab), so the content that was wiped would never be seen. After a plot is rendered, I don't think you can change it can you?

@murrayrm
Comment options

Seems fine to take the matplotlib/pandas approach. But I do think that most people would expect that callingbode_plot twice in a row would do the usual thing of overlapping the plots without extra arguments. So

bode_plot(sys1)bode_plot(sys2)

rather than

fig, ax = bode_plot(sys1)bode_plot(sys2, ax=ax)

(though the latter should also work, of course).

Happy to be overridden and you are writing the code, so you get to choose -:).

@billtubbs
Comment options

Yes, no problem with that. That is what it does (if noaxes argument is given, it gets the current figure and adds the bode plots to the first two axes).

The only debate was what to do if someone tries to add a nyquist plot to a bode plot, or some other strange combination.

Comment options

matplotlib's recommended function signature returns a list ofArtists. This is useful when one wants to change linestyles, add labels for a legend, etc. The axes can easily be recovered from the handles as propertyaxes. The Artists can be recovered from an axes too via theget_children method, but if one wanted to set Artist properties while doing multiple calls to a plot function, one would have to figure out which are the new Artists each time.

You must be logged in to vote
4 replies
@bnavigator
Comment options

bnavigatorJan 15, 2022
Collaborator Author

The link points to the MPL 2.0.2 documentation which is a bit outdated. The closest I could find in the current doc isUsage Patterns.

I totally agree with the conclusion: We should do it the same way as most of thematplotlib.pyplot functions andmatplotlib.axes.Axes methods do it: ReturnArtists collections.

@billtubbs
Comment options

Hi@roryyorke, I'm currently working on this. If you can go through thetable above which is a list of the proposed changes we're making (also compares to the latest Julia ControlSystems equivalents) and see if you think these make sense. As well as having every plot function return a fig and/or axes, we have to come up with alternative ways to calculate the data that these functions previously returned. This is perhaps the trickier part of this proposal. (Although we will always have the original MATLAB equivalents which do plotting and return data in one function).

@roryyorke
Comment options

HI@billtubbs, thanks for working on this.

The changes on the whole make sense; splitting computation from presentation is a good idea.

I'm not sure about using custom classes to represent results; it does look tidy, and as you mentioned it could be handy if one needs metadata.

I love using pandas, but it's a fairly heavy dependency. A pandas-compatibility module, separate from plotting, could be useful, to generate DataFrames from time series and frequency response data.

FWIW, one of my favourite Matlab plotting functions,bodemag, is missing; however, we could add it later (it's hardly complicated if one hasbode).

To expand on my suggestion of returning a list ofArtists instead of the figure or axes, using this would look something like:

h1=nyquist_plot(sys1)ax=h1[0].axesh2=nyquist_plot(sys2,ax=ax)h1[0].set_label('System 1')h2[0].set(label='System 2',linewidth=5,alpha=0.5,linestyle='--')ax.legend()

The equivalent using afig&axes return isn't any more complicated (see below), but I don't know if there's any guarantee on the ordering of Artists in the list returned by Axes.get_children; I googled but could find no discussion of this.

plt.close('all')fig,ax=nyquist_plot2(sys1)nyquist_plot2(sys2,ax=ax)h1,h2=ax.get_children()[:2]h1.set_label('System 1')h2.set(label='System 2',linewidth=5,alpha=0.5,linestyle='--')ax.legend()
@billtubbs
Comment options

@roryyorke I agree with your point aboutbode_mag. In this new paradigm we should not loose any functionality and I think we should keep the underlying functional approach. Here's one way we could achieve the bode_mag plot functionality in the new paradigm:

fr=freqresp(sys,w)plt.figure(1)fr.plot()# make a Bode plot (by default, kind='bode'), returns figplt.figure(2)fr.plot(kind='mag')# make the mag plot, returns axplt.figure(3)fr.plot(kind='phase')# make the phase plot, returns ax

However, we could retain:

plt.figure(1)ct.bode_plot(sys)# returns (fig, axes) not (mag, phase, w) as currentlyplt.figure(2)ct.mag_plot(sys)# returns axplt.figure(3)ct.phase_plot(sys)# returns ax
Comment options

@billtubbs I have put this feature on the list for possible release in 0.10.0 (see#917). Are you still working on this/interested in working on this/have the time to work on this?

I have some ideas of how to do all of this for time series data (using theTimeResponseData class), following some of the suggestions above. I'm thinking I might take that on as a summer project. Let me know the status on your end so that we can try to find a comment set of patterns for implementing all of this.

You must be logged in to vote
7 replies
@billtubbs
Comment options

Actually, I found more work I did dating to Apr 30, 2022 titled 'work in progress' which I have now merged into the master of this fork:
https://github.com/basicmachines/python-control

@billtubbs
Comment options

I added some temporary files that I was using to guide the code development that might be useful to understand what I was doing:

@billtubbs
Comment options

B.t.w. I tried to contact someone on the Julia ControlSystems project (@murrayrm you gave me the name) but they never replied. I was hoping to collaborate and co-ordinate the changes a bit—see table above. I think this would still be worthwhile as some consistency between the projects would be good (but not essential).

I think it's only the last 6 rows of that table (inplotting_readme.md) that I didn't complete. Although there will be more work to do to integrate the stale fork now.

@billtubbs
Comment options

I recall one of the biggest challenges I encountered but I can't remember if I overcame it or not:

  • In the Pandas plotting paradigm, the user can create and prepare a figure and then individual subplots may be added by the user using a keyword (ax=..). However,all Pandas plots use a cartesian grid system (i.e. axis projection). The challenge we have is that in some cases we need a polar grid. Changing an existing axis that the user has created from cartesian to polar is difficult. But I think@bnavigator found a solution and I can't remember if I got it working, sorry!

In matplotlib, you're supposed to specify the projection type when you create the figure. E.g.:

fig,ax=plt.subplots(subplot_kw=dict(projection='polar'))

But if you look in the notebook linked above, I have some code as follows, which suggests I did get it working (almost, look at the plots, there are still some bugs) using aposition keyword argument or in some cases the desiredax keyword:

# Create figure with subplotsfig=plt.figure(figsize=(9,8))sgrid(position=(2,2,1))ax2=fig.add_subplot(2,2,2)zgrid(ax=ax2)# alternative for zgrids#zgrid(position=(2, 2, 2))sgrid(position=(2,2,3))ax4=fig.add_subplot(2,2,4)zgrid(ax=ax4)# alternative for zgridsplt.tight_layout()savefig('grids_subplots')
@billtubbs
Comment options

This is probably where I got bogged down and should have called for help!

Comment options

I've taken a first crack at implementing the new plotting paradigm (#645) for the time response functions. The following code

sys = ct.rss(4, 2, 2)ct.step_response(sys).plot()

now produces:
step_response

You can still do old style plotting using this code

response = ct.step_response(sys)t, y = responseplt.plot(t, y[0, 0], t, y[1, 0])

(note thaty has two indices because a MIMO step response has multiple traces. Soy[1, 0] refers toy[1] for the step response corresponding to a step inu[0]).

Or, if you prefer, you can call the plot function explicitly:

ct.time_response_plot(response)

There are lots of options for changing around the way things plot, allowing multiple calls (which plot on the same axes), and more. Some preliminary documentation ishere.

I'll post this as a PR as soon as#916 gets merged into the main branch (since this set of changes builds on that one).

You must be logged in to vote
11 replies
@billtubbs
Comment options

Another question: If you define a system with named input and output signals, do the names appear on the plot axes?

@murrayrm
Comment options

Yup.

sys = ct.rss(4, inputs=['velocity', 'steering'], outputs=['xpos', 'ypos'], name='car'])ct.step_response(sys).plot()

named_signals

@billtubbs
Comment options

Final thing for me, would be to pass unrecognized keyword arguments on to the plot function.
E.g.

sys=ct.rss(4,2,2)ct.step_response(sys).plot(linewidth=2,linestyle='--')
@murrayrm
Comment options

Good point. I've got that on my list to implement prior to the non-draft PR, but haven't done anything on it yet. One question is going to be how to handle multi-signal and/or multi-trace plots. So in your example above, I would assume that all lines are width 2 and dashed. But what if I want to combine outputs onto a single axes (using combine_outputs=True) and then specify different colors/width/styles for each? Perhaps

ct.step_response(sys).plot(    output_color=['k', 'b'], trace_linestyle=['-', --'], combine_signals=True, combine_traces=True)

which would produce something like this (created by updating the linestyles manually, using the returned lines):
linestyles
(the trailing 'sys[65]' in the legend will go away when implemented).

@murrayrm
Comment options

See PR#920 for the changes that implement the time response functions above.

Moving on to frequency response functions (Bode, Nyquist) next.

Comment options

See PR#924 for frequency response implementation.

Moving on to pole-zero maps (pzmap, rlocus).

You must be logged in to vote
0 replies
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Category
Ideas
Labels
None yet
6 participants
@bnavigator@roryyorke@murrayrm@vincentchoqueuse@billtubbs@sawyerbfuller

[8]ページ先頭

©2009-2025 Movatter.jp