-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Add graphical analyses in physics.control
#21763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
✅ Hi, I am the SymPy bot (v161). I'm here to help you write a release notes entry. Please read the guide on how to write release notes. Your release notes are in good order. Here is what the release notes will look like:
This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.9. Click here to see the pull request description that was parsed.
Update The release notes on the wiki have been updated. |
🟠Hi, I am the SymPy bot (v161). I've noticed that some of your commits add or delete files. Since this is sometimes done unintentionally, I wanted to alert you about it. This is an experimental feature of SymPy Bot. If you have any feedback on it, please comment at sympy/sympy-bot#75. The following commits add new files:
If these files were added/deleted on purpose, you can ignore this message. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These plots look really nice!
(For @namannimmo10, the text-overflow/overlap shouldn't be an issue, in my opinion, right? |
Is there some way to fix the axis on the extreme left instead of floating in the middle of the screen (like in MATLAB)? |
Benchmark results from GitHub Actions Lower numbers are good, higher numbers are bad. A ratio less than 1 Significantly changed benchmark results (PR vs master) Significantly changed benchmark results (master vs previous release) before after ratio
[ed9a550f] [ad4cbf28]
<sympy-1.8^0>
- 1.11±0.01s 151±0.4ms 0.14 dsolve.TimeDsolve01.time_dsolve
- 10.3±0.01s 5.04±0s 0.49 integrate.TimeIntegrationRisch02.time_doit(100)
- 10.2±0.01s 5.03±0s 0.49 integrate.TimeIntegrationRisch02.time_doit_risch(100)
- 85.2±0.6μs 33.3±0.2μs 0.39 matrices.TimeDiagonalEigenvals.time_eigenvals
- 106±2μs 66.6±0.7μs 0.63 matrices.TimeMatrixGetItem.time_ImmutableDenseMatrix_getitem
- 106±0.7μs 67.1±1μs 0.64 matrices.TimeMatrixGetItem.time_ImmutableSparseMatrix_getitem
- 104±0.3μs 67.2±1μs 0.65 matrices.TimeMatrixGetItem.time_MutableSparseMatrix_getitem
- 96.6±0.4μs 60.8±0.06μs 0.63 solve.TimeMatrixArithmetic.time_dense_add(10, 0)
+ 13.2±0.05μs 21.9±0.2μs 1.66 solve.TimeMatrixArithmetic.time_dense_add(3, 0)
+ 15.5±0.05μs 33.7±0.3μs 2.17 solve.TimeMatrixArithmetic.time_dense_add(3, 5)
+ 22.9±0.08μs 43.3±0.09μs 1.89 solve.TimeMatrixArithmetic.time_dense_add(4, 5)
+ 42.1±0.2μs 70.5±0.2μs 1.67 solve.TimeMatrixArithmetic.time_dense_add(6, 5)
- 1.42±0.01ms 260±0.6μs 0.18 solve.TimeMatrixArithmetic.time_dense_multiply(10, 0)
- 55.0±0.4μs 28.3±0.1μs 0.52 solve.TimeMatrixArithmetic.time_dense_multiply(3, 0)
+ 83.7±0.8μs 151±0.9μs 1.81 solve.TimeMatrixArithmetic.time_dense_multiply(3, 5)
- 117±0.6μs 36.0±0.07μs 0.31 solve.TimeMatrixArithmetic.time_dense_multiply(4, 0)
- 342±2μs 74.6±0.1μs 0.22 solve.TimeMatrixArithmetic.time_dense_multiply(6, 0)
- 104±0.6μs 47.1±0.2μs 0.45 solve.TimeMatrixOperations.time_det(3, 0)
- 189±0.4μs 113±0.3μs 0.60 solve.TimeMatrixOperations.time_det(3, 2)
- 179±0.5μs 104±0.8μs 0.58 solve.TimeMatrixOperations.time_det(3, 5)
- 106±3μs 47.1±0.1μs 0.44 solve.TimeMatrixOperations.time_det_bareiss(3, 0)
- 191±1μs 113±0.3μs 0.59 solve.TimeMatrixOperations.time_det_bareiss(3, 2)
- 182±2μs 104±0.2μs 0.57 solve.TimeMatrixOperations.time_det_bareiss(3, 5)
- 106±0.4μs 47.3±0.4μs 0.45 solve.TimeMatrixOperations.time_det_berkowitz(3, 0)
- 191±1μs 114±2μs 0.60 solve.TimeMatrixOperations.time_det_berkowitz(3, 2)
- 179±0.3μs 106±0.7μs 0.59 solve.TimeMatrixOperations.time_det_berkowitz(3, 5)
+ 822±5μs 1.31±0.01ms 1.59 solve.TimeMatrixOperations.time_det_berkowitz(4, 0)
+ 982±2μs 1.87±0.03ms 1.90 solve.TimeMatrixOperations.time_det_berkowitz(4, 2)
+ 1.02±0ms 1.98±0.01ms 1.94 solve.TimeMatrixOperations.time_det_berkowitz(4, 5)
+ 303±0.8μs 501±1μs 1.65 solve.TimeMatrixOperations.time_rank(3, 0)
+ 474±0.5μs 756±1μs 1.59 solve.TimeMatrixOperations.time_rank(4, 0)
+ 140±0.1μs 210±0.4μs 1.50 solve.TimeMatrixOperations.time_rref(3, 0)
- 5.97±0.01ms 3.29±0.03ms 0.55 solve.TimeRationalSystem.time_linsolve(10)
- 1.20±0.01ms 760±4μs 0.63 solve.TimeRationalSystem.time_linsolve(5)
+ 7.42±0.01ms 11.2±0.03ms 1.51 solve.TimeSparseSystem.time_linear_eq_to_matrix(30)
- 3.90±0.03ms 2.47±0.03ms 0.63 solve.TimeSparseSystem.time_linsolve_eqs(30)
Full benchmark results can be found as artifacts in GitHub Actions |
I'm not aware. If there's a way to obtain a better-looking (with no overlaps or overflows) bode plot using matplotlib or other backends, you can go with that. |
With this, all the plots are complete. Let me know if you find any issues. |
""" | ||
system = system.doit() # Get the equivalent TransferFunction object. | ||
|
||
num_poly = Poly(system.num, system.var).all_coeffs() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not use Poly
for this implementation; It doesn't work well with time-delay terms.
>>> G = TransferFunction(s*exp(-s), s**2 + 2*s + 1, s)
>>> pole_zero(G)
Traceback (most recent call last):
File "/home/namannimmo/oss/sympy/sympy/polys/polyutils.py", line 211, in _parallel_dict_from_expr_if_gens
monom[indices[base]] = exp
KeyError: exp(-s)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/namannimmo/oss/sympy/sympy/physics/control/control_plots.py", line 28, in pole_zero
num_poly = Poly(system.num, system.var).all_coeffs()
File "/home/namannimmo/oss/sympy/sympy/polys/polytools.py", line 164, in __new__
return cls._from_expr(rep, opt)
File "/home/namannimmo/oss/sympy/sympy/polys/polytools.py", line 293, in _from_expr
rep, opt = _dict_from_expr(rep, opt)
File "/home/namannimmo/oss/sympy/sympy/polys/polyutils.py", line 368, in _dict_from_expr
rep, gens = _dict_from_expr_if_gens(expr, opt)
File "/home/namannimmo/oss/sympy/sympy/polys/polyutils.py", line 307, in _dict_from_expr_if_gens
(poly,), gens = _parallel_dict_from_expr_if_gens((expr,), opt)
File "/home/namannimmo/oss/sympy/sympy/polys/polyutils.py", line 216, in _parallel_dict_from_expr_if_gens
raise PolynomialError("%s contains an element of "
sympy.polys.polyerrors.PolynomialError: exp(-s) contains an element of the set of generators.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason behind using Poly()
was speed. I was using NumPy for root-locus and getting a huge performance advantage because- (a) SymPy is a symbolic computation library and NumPy is a numeric computation library. (b) Matplotlib requires NumPy for internal numeric computation and hence NumPy is also a listed dependency for matplotlib. It is installed with matplotlib.
Also just to be clear, these plots will not support transfer functions with time delay. Maybe I'll allow time-delay in Pole-Zero because time delay doesn't affect the poles and zeroes of the system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible to use sympy with other plotting backends that do not necessarily use numpy:
https://pypi.org/project/sympy-plot-backends/
Generally when sympy has numeric routines they are more accurate than numpy's:
In [8]: p = expand(prod(x - i for i in range(1, 21)))
In [9]: nroots(p)
Out[9]:
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15
.0, 16.0, 17.0, 18.0, 19.0, 20.0]
In [10]: numpy.roots(Poly(p).all_coeffs())
Out[10]:
array([19.99980929, 19.00190982, 17.99092135, 17.02542715, 15.94628672,
15.0754938 , 13.91475559, 13.07431403, 11.95328325, 11.02502293,
9.99041304, 9.00291529, 7.99935583, 7.000102 , 5.99998925,
5.00000067, 3.99999998, 3. , 2. , 1. ])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plotting backend is not an issue here. Whichever backend we may use, it requires efficient sampling to generate plots. Some form of numerical computation will always occur under the hood. SymPy undoubtedly is more accurate in numerical operations. Still, during sampling with a large number of points (10000), accuracy hardly matters, especially when a much faster approach (more than 1000 times) is accurate up to 3-4 decimal places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is accurate up to 3-4 decimal places.
The accuracy can degrade much more than that.
The idea with using a symbolic library is that you should have greater confidence in things like the accuracy of any computed result even if that means waiting longer for things to be computed. It also means that different approaches are often better to make something efficient.
As an example your root_locus
function computes the roots of 10000 different polynomials. Why 10000? No one needs that many points in the plot. Many of the lines are just joining points on the real axis. To plot a straight line you only need to know its start and end and those can be computed exactly. Then you don't need to evaluate the polynomial at 10000 points.
I'm not sure I've understood the calculation that you want precisely but I think it's something like this:
In [35]: eq
Out[35]: k⋅(s + 7) + s⋅(s + 5)⋅(s + 15)⋅(s + 20)
In [36]: discriminant(eq)
Out[36]:
4 3 2
- 27⋅k + 47088⋅k - 37907500⋅k + 3881250000⋅k + 1265625000000
In [37]: nroots(discriminant(eq))
Out[37]: [-130.564918275438, 307.765516989741, 783.399700642849 - 743.513266939402⋅ⅈ, 783.399700642849 + 743.513266939402⋅ⅈ]
The positive real roots for k
are the points on the real axis where lines start and end. This now shows you what values of k
are interesting for the plot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For TransferFunction(2*s**2 + 5*s + 1, s**2 + 3*s + 5, s)
-
If
num=10000
(sampled 10000 times, default)The scatter plot looks like this.
And the actual root-locus plot (corresponding to the regression of these sampled points) looks like this-
Similarly,
To plot a straight line you only need to know its start and end and those can be computed exactly. Then you don't need to evaluate the polynomial at 10000 points.
Straight Lines in root locus plots are not very common. In root-locus plots, we plot the movement of the roots of the polynomial den+ k*num
when k varies from 0 to inf. As you can tell when k is 0 we get the roots of den (poles) and as k approaches inf, the roots of den + k*num
are roots of num only (zeros). Therefore, when num and den have the same degree, the root locus plot is a path from poles to zeros of the transfer function. This is not always a straight line.
Only one is a straight line path here and knowing this fact can't help us in any way. I don't understand how knowing the discriminant of the polynomial will help us in knowing the points on the rl plot. Maybe I'm missing something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Straight Lines in root locus plots are not very common.
Your example with num=1000
illustrates the point I wanted to make perfectly. To generate the plot you compute the roots of the polynomial for 1000 different values of k ranging from 0 to kmax where kmax is arbitrarily chosen to be 400. Out of those 1000 values of k only 4 have non-real roots so the other 996 polynomials whose roots are computed are only used to plot two straight lines. The straight line could be computed more efficiently and more accurately by simply computing its start and end points which you can do like this:
In [93]: k, s = symbols('k, s')
In [94]: n, d = 2*s**2 + 5*s + 1, s**2 + 3*s + 5
In [95]: eq = d + k*n
In [96]: eq
Out[96]:
⎛ 2 ⎞ 2
k⋅⎝2⋅s + 5⋅s + 1⎠ + s + 3⋅s + 5
In [97]: nroots(n) # k = oo
Out[97]: [-2.28077640640441, -0.219223593595585]
In [98]: discriminant(eq, s)
Out[98]:
2
17⋅k - 14⋅k - 11
In [99]: nroots(discriminant(eq, s))
Out[99]: [-0.491899499749248, 1.31542891151395]
In [100]: roots(discriminant(eq, s), multiple=True)
Out[100]:
⎡7 2⋅√59 7 2⋅√59⎤
⎢── - ─────, ── + ─────⎥
⎣17 17 17 17 ⎦
In [101]: eq.subs(k, roots(discriminant(eq, s), multiple=True)[1])
Out[101]:
2 ⎛7 2⋅√59⎞ ⎛ 2 ⎞
s + 3⋅s + ⎜── + ─────⎟⋅⎝2⋅s + 5⋅s + 1⎠ + 5
⎝17 17 ⎠
In [102]: roots(eq.subs(k, roots(discriminant(eq, s), multiple=True)[1]), s)
Out[102]: {-9 + √59: 2}
In [103]: list(_)[0].n()
Out[103]: -1.31885425213139
Your suggestion to use num=10000 and compute the roots of 10000 polynomials is a workaround for having the wrong value of kmax. In fact by computing the roots of the discriminant it is possible to compute what the relevant values of k are.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine. I get your point now. But there is still an issue. You see, with your calculations, we get these points:
Which is basically poles and zeroes of the Transfer Function + The point where the discriminant is zero (equal roots). I agree that for the two straight lines on the axis, this will work fine but what about the two symmetric arms of the curve?
We cannot plot them by simply knowing the start and endpoints. We still need to compute the roots of the polynomials from 0 to k_c
(value of k for which the 0 discriminant point is reached) in uniform intervals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need to compute the roots of the polynomials from 0 to
k_c
Yes, but we don't need 10000 points to plot the curve. The num=10000
plot above shows that an acceptable plot is obtained from only 40 points (since the other 9960 points are on the real axis).
I expect that uniform spacing is not optimal. Naturally I would guess that something like logarithmic or harmonic spacing would be better. You can see in the num=10000
plot that using uniform intervals for k
does not lead to a uniform distance between the points in the plot.
Ideally the curve would be generated using pseudo-arc-length continuation so you can generate them at a fixed 2D distance within the plot but that's probably more complicated than it is worth.
Looks very nice ! Much appreciated For the step responses, I'd suggest my annoying pair of systems I'm a bit worried about the inverse laplace way since fast poles might trip up the exponential calculations say pole at -120 is no problem for numeric step responses but exp(-120*t) might underflow too quickly. Emphasis on might :D |
You can set the |
Although this is a cool and much wanted feature, I'm not that sure whether it makes sense to put these plotting functions in as public API. Although it would be nice to have out-of-the box plotting tools, we end up with a lot of customization and localization issues which eventually forces users to create a copy of existing functionality anyway if they want to do even the simplest things like changing the plot titles and labels. |
I think that the solution to that is that the plotting APIs should expose the lower level a bit so it's easy to e.g. get the matplotlib handle for the figure and add a title. At the same time any plotting APIs should make it easier to just get the underlying data so that they can be used with different plotting libraries. CC @Davide-sd |
physics.control
physics.control
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Can you finish this off by showing in the PR how all the plots look like?
Can you check locally whether it is all rendering fine? Some cleanups might also be needed. We can improve these plots in the future. I can merge this tonight. |
Do you mean the docs? |
Yes! |
Have a look. Updated to the latest commit. Edit: https://docs.sympy.org/dev/modules/physics/control/control_plots.html Now you can see it here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Thank you!
Having just released sympy 1.9 I've found that the tests added here fail: $ bin/test sympy/physics/control/tests/test_control_plots.py
======================================== test process starts ========================================
executable: /home/oscar/current/sympy/37venv/bin/python (3.7.10-final-0) [CPython]
architecture: 64-bit
cache: yes
ground types: gmpy 2.0.8
numpy: 1.21.0
random seed: 40766605
hash randomization: on (PYTHONHASHSEED=3506573775)
sympy/physics/control/tests/test_control_plots.py[6] .F.FFF [FAIL]
_____________________________________________________________________________________________________
_________________ sympy/physics/control/tests/test_control_plots.py:test_pole_zero __________________
Traceback (most recent call last):
File "/home/oscar/current/sympy/sympy.git/sympy/physics/control/tests/test_control_plots.py", line 100, in test_pole_zero
((0.0,), ((-0.5000000000000004+0.8660254037844395j),
AssertionError
_____________________________________________________________________________________________________
______________ sympy/physics/control/tests/test_control_plots.py:test_impulse_response ______________
Traceback (most recent call last):
File "/home/oscar/current/sympy/sympy.git/sympy/physics/control/tests/test_control_plots.py", line 192, in test_impulse_response
assert impulse_res_tester(tf3, exp3)
AssertionError
_____________________________________________________________________________________________________
_______________ sympy/physics/control/tests/test_control_plots.py:test_step_response ________________
Traceback (most recent call last):
File "/home/oscar/current/sympy/sympy.git/sympy/physics/control/tests/test_control_plots.py", line 241, in test_step_response
assert step_res_tester(tf3, exp3)
AssertionError
_____________________________________________________________________________________________________
_______________ sympy/physics/control/tests/test_control_plots.py:test_ramp_response ________________
Traceback (most recent call last):
File "/home/oscar/current/sympy/sympy.git/sympy/physics/control/tests/test_control_plots.py", line 283, in test_ramp_response
assert ramp_res_tester(tf3, 10, exp3, 1.5)
AssertionError
======================== tests finished: 2 passed, 4 failed, in 4.73 seconds ========================
DO *NOT* COMMIT!
I guess these tests are not properly running in CI and also they seem to be incorrect. |
I will try to debug it soon. |
References to other Issues or PRs
Brief description of what is fixed or changed
Implement common graphical analyses related to control theory.
Other comments
Checklist:
Future Work:
Root Locus(Effective sampling needed. See Add graphical analyses inphysics.control
#21763 (comment) and Add graphical analyses inphysics.control
#21763 (comment))Nyquist Plot **(Out of scope of my proposed project. Will add sometime in the future along with Nichols plot)Release Notes