GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — New plotting paradigm · python-control/python-control · Discussion #645

New plotting paradigm #645

bnavigator started this conversation in Ideas
New plotting paradigm #645
@bnavigator bnavigator
Aug 10, 2021 · 22 comments · 58 replies
Return to top
Discussion options
  • Uh oh!

    There was an error while loading. Please reload this page.

bnavigator
Aug 10, 2021
Collaborator

@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_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 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.

You must be logged in to vote
All reactions

Replies: 22 comments · 58 replies

Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

bnavigator
Aug 10, 2021
Collaborator Author

I like it very much.

We can keep the root_locus, bode_plot, and nyquist_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
All reactions
0 replies
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Aug 10, 2021

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. functions sgrid and zgrid calling plt.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
All reactions
0 replies
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm
Aug 10, 2021
Maintainer

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 a TimeSeries object with data elements time, input, output, state (or t, u, y, x?) and we could have a plot method.

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

You must be logged in to vote
All reactions
1 reply
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Aug 10, 2021

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 Pandas Series and DataFrame 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).

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Aug 10, 2021

Found another example of this paradigm from scipy.spatial here:

import matplotlib.pyplot as plt
import numpy as np
from scipy.spatial import ConvexHull, convex_hull_plot_2d
points = np.random.random((30, 2))
hull = ConvexHull(points)
_ = convex_hull_plot_2d(hull)
plt.show()

Here, they use a separate function convex_hull_plot_2d for the plotting step and it returns a hook to the whole figure. Not sure why they didn't give ConvexHull a plot_2d() method.
Same thing for scipy.spatial.Voronoi and scipy.spatial.voronoi_plot_2d.

You must be logged in to vote
All reactions
0 replies
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

sawyerbfuller
Aug 10, 2021
Collaborator

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 like rlocus into computation, e.g. root_locus and plotting, e.g. root_locus_plot? *

import control as ct
fig, 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")```

* outside of the matlab layer, frequency response plots largely exist: ``nyquist_plot``, ``bode_plot``, ``nichols_plot``, but time response plots don't seem to, e.g. ``step_plot``, ``impulse_plot``
You must be logged in to vote
All reactions
2 replies
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Aug 11, 2021

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), where df 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 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?

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).

All reactions
@sawyerbfuller
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

sawyerbfuller Aug 11, 2021
Collaborator

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 store frequency_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 a TimeResponseData (Or TimeSeriesData) object, with an optional input 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 for root_locus?

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Aug 11, 2021

@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 la plt.subplots) or as multiple lines on a single set of axes? Just call the plot 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
All reactions
0 replies
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm
Aug 21, 2021
Maintainer

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

You must be logged in to vote
All reactions
  • 👍 1
5 replies
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Aug 21, 2021

It also says this at the bottom:

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

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Aug 21, 2021

Had a quick look at the Julia proposal. Looks like functions like lsim, step, etc. will in future return a SimResult(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()
All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Aug 21, 2021

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

yout, t, xout = lsim(sys, u, t, x0)
All reactions
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Aug 22, 2021
Maintainer

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 class InputOutputResponse 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 (like plot).

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.

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Dec 31, 2021

For the input-output response, it would be nice (IMO) if response.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', ...).

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

vincentchoqueuse
Aug 23, 2021

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

  • repo: https://github.com/vincentchoqueuse/python-control-plotly
  • gallery: https://python-control-plotly.readthedocs.io/en/latest/documentation/gallery.html
You must be logged in to vote
All reactions
0 replies
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Dec 30, 2021

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

Function Purpose
bode Compute the magnitude and phase of the frequency response
bodeplot Create a Bode plot of the system
freqresp Evaluate the frequency response of a linear system
gangoffourplot Gang-of-Four plot
margin Compute frequencies for gain margins, gain margins, frequencies for phase margins, phase margins
marginplot Plot all the amplitude and phase margins of the system
nicholsplot Create a Nichols plot of the system
nyquist Compute the real and imaginary parts of the frequency response
nyquistplot Create a Nyquist plot of the system
pzmap Create a pole-zero map of the system
rlocus Computes and plots the root locus of the SISO LTISystem (deprecated)
rlocusplot Computes and plots the root locus of the SISO LTISystem
sigma Compute the singular values sv of the frequency response
sigmaplot Plot 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-Control root_locus function root_locus_plot? All our other plot functions are clearly differentiable from their calculation-only equivalents.

You must be logged in to vote
All reactions
0 replies
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Dec 31, 2021

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?

Julia Python-Control current Proposal
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/a n/a fr.plot()
bodeplot(sys) bode_plot(sys) no change
fig = bodeplot(sys) n/a fig, axes = bode_plot(sys)
gangoffourplot(P, C) gangof4_plot(P, C) no change
fig = gangoffourplot(P, C) n/a fig, axes = gangof4_plot(P, C)
ωgm, gm, ωpm, pm = margin(sys) gm, pm, wg, wp = margin(sys) no change
marginplot(sys) n/a no change for now
nicholsplot(sys) nichols_plot(sys) no change
fig = nicholsplot(sys) n/a ax = nichols_plot(sys)
n/a n/a nichols_plot(sys, ax=ax)
re, im, w = nyquist(sys) n/a no change for now
nyquistplot(sys) nyquist_plot(sys) no change
fig = nyquistplot(sys) n/a ax = nyquist_plot(sys)
n/a n/a nyquist_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/a ax = pole_zero_map(sys)
n/a n/a pole_zero_map(sys, ax=ax)
n/a roots, k_out = root_locus(sys, plot=None) roots, k_out = root_locus(sys)
rlocusplot(sys) root_locus(sys) root_locus_plot(sys)
n/a n/a ax = root_locus_plot(sys)
n/a n/a root_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/a n/a ax = singular_values_plot(sys)
n/a n/a singular_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/a n/a ax = response.plot()*
n/a n/a response.plot(ax=ax)*

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

You must be logged in to vote
All reactions
0 replies
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

sawyerbfuller
Dec 31, 2021
Collaborator

Hi @billtubbs,

  • an alternative to freqresp(sys, w) is the more pythonic mag, phase, w = sys.frequency_response(w) Remarks: 1. It occurs to me there should probably also be a function frequency_response(sys,w). 2. A todo is to change its output to be simply a complex array eg x = 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 also stability_margin That returns the stability margin in addition to gain and phase margins. I have also often wanted a margin_plot Function.
  • I think there might now be a singular_values_plot Function.
  • there are already pole And zero Functions so that is already done.
You must be logged in to vote
All reactions
2 replies
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Dec 31, 2021

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 of sys 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 the sys object at the moment unless others think it's important. We can always add these later once the basic plotting functions are sorted out.

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Dec 31, 2021

I think there might now be a singular_values_plot Function.

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

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm
Dec 31, 2021
Maintainer

A few comments:

  • Regarding root_locus versus root_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 is pzmap: should this be pole_zero_plot? Or perhaps pole_zero_map? (Note that we can always set pzmap = pole_zero_plot to allow the old usage and we should make sure that the MATLAB compatibility module maintains MATLAB names and signatures. I like pole_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 for bode_plot?).

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

  • I like frequency_response over freqresp, especially since we deprecated the latter in REBASE: use __call__ instead of evalfr in lti system classes  #499.

  • Perhaps we should (eventually) allow either ct.method(sys) or sys.method() to work, similar to NumPy. Calling ct.method(sys) would then just call the appropriate method.

  • For the pzmap() function: perhaps this should accept a tuple as its input argument (in addition to a system). So you could say pole_zero_plot((poles, zeros)) or pole_zero_plot(sys).

You must be logged in to vote
All reactions
8 replies
@sawyerbfuller
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

sawyerbfuller Jan 1, 2022
Collaborator

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

In regards to the frd Class, it would be nice if we could re-use it to hold data coming out of eg frequency_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 to frequency_response, my recollection was that there was consensus that it was an accident that it outputs mag, phase, w. It should change to output response as complex number. If you want mag, phase, w, maybe you could use the new plot-free bode 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.

All reactions
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jan 1, 2022
Maintainer

For the return value for sys.frequency_response, you can set things up so that this is a FrequencyResponseData object and then set up an __iter__ method that returns either mag, phase, omega (default) or s, omega. Similarly, you can use the property 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 real
mag, phase, w = fr                        # implement via __iter__
mag, phase, w = fr.mag, fr.phase, fr.w    # implement via @property
d, w = fr(return_complex=True)            # implement via __iter__
fr.plot(kind='bode')
ct.bode_plot(sys)                         # same plot
ct.bode_plot(fr)                          # same plot
fr.plot(kind='nyquist')
ct.nyquist_plot(sys)                      # same plot
ct.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 that TimeResponseData 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 the FrequencyResponseData object (perhaps a separate attribute?).

Finally, the current code for nyquist_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.

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 1, 2022

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)
All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 1, 2022

Oh, I see that TimeResponseData uses this approach with return_x for backwards compatibility. Okay, not a bad idea.

All reactions
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jan 1, 2022
Maintainer

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).

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Jan 4, 2022

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

>> load AnalyzerData
>> sys = frd(resp,freq);
>> sys

sys =
 
    Frequency(rad/s)            Response       
    ----------------            --------       
          0.1000         2.384e-01 - 1.951e-03i
          0.1027         2.540e-01 - 3.511e-03i
          0.1056         2.430e-01 + 1.807e-03i
          0.1085         2.573e-01 - 4.672e-03i
          0.1114         2.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
All reactions
6 replies
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 4, 2022

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, [1 2*0.2 1]);
>> [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);
All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 4, 2022

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

>> sys = tf(1, conv([-0.5i 1],[0.5i 1]));
>> 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

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 4, 2022

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?

All reactions
@bnavigator
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

bnavigator Jan 4, 2022
Collaborator Author

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

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 4, 2022

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

All reactions
  • 👍 1
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Jan 5, 2022

I just noticed this comment in bode_plot in freqplot.py:

python-control/control/freqplot.py

Lines 311 to 312 in a111b03

# 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 individual bode_mag and bode_phase plots (returning the ax only) so the user can construct whatever they want with those.

You must be logged in to vote
All reactions
1 reply
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 5, 2022

How about this solution:

bode_mag_plot(syslist)  # quick plot
bode_mag_plot(syslist, ax=ax)  # adding to existing figure
ax = bode_mag_plot(syslist)  # returning axis handle

and same for bode_phase_plot()

And then in the new paradigm:

fr = frequency_response(syslist)
# quick plot
fr.plot(kind='bode')
fr.plot(kind='bode_mag')
fr.plot(kind='bode_phase')
# or
fig, axes = fr.plot(kind='bode')
fr.plot(kind='bode_mag', ax=ax)
ax = fr.plot(kind='bode_phase')
... etc
All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Jan 7, 2022

Can anyone think of a good name for this function to provide the calculations we are proposing to remove from nyquist_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 return counts and contours as attributes (properties) of the FrequencyResponseData object. Still, we might need better names for them. "counts" is a bit vague when out of context, but something like number_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 called nyquist_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
All reactions
1 reply
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jan 8, 2022
Maintainer

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.

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Jan 8, 2022

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 previous mag, 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
  • New ax = pole_zero_plot(sys, ax=ax) function - complete
  • New ax = root_locus_plot(sys, ax=ax) function
  • New ax = nichols_plot(sys, ax=ax) function
  • New ax = singular_values_plot(sys) function - complete
  • New sigma, w = singular_values(sys) function. - complete

Stage 2 - Introduce new plotting paradigm for frequency response

  • Add new properties to FrequencyResponseData object (mag, phase, nyquist_analysis, etc.)
  • Modify frequency_response function to return FrequencyResponseData object
  • Add plot methods to FrequencyResponseData object
  • Possibly make a pole_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
All reactions
0 replies
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm
Jan 8, 2022
Maintainer

@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
All reactions
5 replies
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 8, 2022

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.

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 8, 2022

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

All reactions
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jan 8, 2022
Maintainer

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.

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 8, 2022

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

All reactions
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jan 8, 2022
Maintainer

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.

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs
Jan 9, 2022

Just to confirm. I'm finding code like this in some of the current plotting functions, E.g. from singular_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
All reactions
5 replies
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jan 9, 2022
Maintainer

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?).

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 10, 2022

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.

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 10, 2022

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?

All reactions
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jan 10, 2022
Maintainer

Seems fine to take the matplotlib/pandas approach. But I do think that most people would expect that calling bode_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 -:).

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 10, 2022

Yes, no problem with that. That is what it does (if no axes 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.

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

roryyorke
Jan 15, 2022

matplotlib's recommended function signature returns a list of Artists. 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 property axes. The Artists can be recovered from an axes too via the get_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
All reactions
4 replies
@bnavigator
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

bnavigator Jan 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 is Usage Patterns.

I totally agree with the conclusion: We should do it the same way as most of the matplotlib.pyplot functions and matplotlib.axes.Axes methods do it: Return Artists collections.

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jan 16, 2022

Hi @roryyorke, I'm currently working on this. If you can go through the table 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).

All reactions
@roryyorke
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

roryyorke Jan 22, 2022

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 has bode).

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

h1 = nyquist_plot(sys1)
ax = h1[0].axes
h2 = 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 a fig&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()
All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jun 21, 2023

@roryyorke I agree with your point about bode_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 fig

plt.figure(2)
fr.plot(kind='mag')  # make the mag plot, returns ax

plt.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 currently

plt.figure(2)
ct.mag_plot(sys)  # returns ax

plt.figure(3)
ct.phase_plot(sys)  # returns ax
All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm
Jun 20, 2023
Maintainer

@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 the TimeResponseData 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
All reactions
7 replies
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jun 21, 2023

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

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jun 21, 2023

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

  • plotting_readme.md
  • Tests-of-Plotting-Tool-Modifications.ipynb
All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jun 21, 2023

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 (in plotting_readme.md) that I didn't complete. Although there will be more work to do to integrate the stale fork now.

All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jun 21, 2023

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 a position keyword argument or in some cases the desired ax keyword:

# Create figure with subplots
fig = 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 zgrids
plt.tight_layout()
savefig('grids_subplots')
All reactions
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jun 21, 2023

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

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm
Jun 30, 2023
Maintainer

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 = response
plt.plot(t, y[0, 0], t, y[1, 0])

(note that y has two indices because a MIMO step response has multiple traces. So y[1, 0] refers to y[1] for the step response corresponding to a step in u[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 is here.

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
All reactions
11 replies
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jul 1, 2023

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

All reactions
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jul 1, 2023
Maintainer

Yup.

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

named_signals

All reactions
  • 👍 1
@billtubbs
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

billtubbs Jul 1, 2023

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='--')
All reactions
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jul 1, 2023
Maintainer

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).

All reactions
@murrayrm
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm Jul 3, 2023
Maintainer

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

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

All reactions
Comment options
  • Uh oh!

    There was an error while loading. Please reload this page.

murrayrm
Jul 22, 2023
Maintainer

See PR #924 for frequency response implementation.

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

You must be logged in to vote
All reactions
0 replies
Sign up for free to 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
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.