-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Improved support for custom labels and rendering options in plotting functions #25531
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. 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.13. 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. 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:
The following commits delete files:
If these files were added/deleted on purpose, you can ignore this message. |
@oscarbenjamin tests are failing for things that I haven't touched. Do I need to rebase or something? |
The other failure is mypy and I'm not sure what about this PR has caused that to change but I do see these errors regularly when running mypy over specific modules (but not the whole codebase):
The functions that it complains about are all functions that have the same name as the module that contains them. Actually these imports are ambiguous:
The ambiguity is here is whether that means the module or the sympy.simplify.trigsimp or the function trigsimp that is defined in that module. It is a poor design in sympy that package __init__ files import things that overwrite submodule names.
The solution is to make these imports fully explicit like
|
The documentation needs to be carefully reviewed for this, |
Also, I'd question the API design if label should be consistently inside the dictionary, than being positional argument, because it is optional.
|
Of course there is, you can find it here. |
There is nothing stopping you from executing something like: plot(
(sin(x), (x, -pi, pi), "a"),
(cos(x), (x, 0, pi), {"linestyle": "--", "label": "b"})
) It is pretty clear that you save some typing if you use the positional argument, which is why it was created. In my experience, it is often necessary to change the label, whereas changing other rendering options like color, linestyle, ..., is less frequent. |
I also have question that |
There is no reason for using both at the same time: either one or the other. Someone is comfortable with the positional arguments, others are comfortable with keyword arguments. In any case, if keywords arguments are specified, they have the priority because they are applied after the data series have been generated. In my opinion, positional arguments have the advantage of making code cleaner. Consider again this code that uses positional arguments: plot(
(sin(x), (x, -pi, pi), "a"),
(cos(x), (x, 0, pi), {"linestyle": "--"})
) Here, anyone reading the code can immediately understand that the label A similar result can be achieved with keyword arguments: plot(
(sin(x), (x, -pi, pi)),
(cos(x), (x, 0, pi)),
label=["a", None],
rendering_kw=[None, {"linestyle": "--"}]
) Here, things are separated into keyword arguments. However, someone reading this code needs to be careful in order to understand that there is a one-to-one correspondence between the symbolic expression and what is specified on the Moreover, now you have specified |
I don't favor the |
If you allows to plot only one expression for each function call, you break back-compatibility. If you allows a plotting function to plot multiple expressions (as is the case right now), users expects further customization (#22609 just to name one)... From what I understand you would like only one way to obtain some result. Correct me if I'm wrong, but you'd prefer something like this: from sympy import *
from spb import *
var("x")
p1 = plot(cos(x + pi / 2) * exp(-x / 4), (x, 0, 2 * pi),
label="oscillator",
ylim=(-1.25, 1.25), show=False)
p2 = plot(exp(-x / 4), (x, 0, 2 * pi),
label="upper limit",
rendering_kw={"linestyle": ":"}, show=False)
p3 = plot(-exp(-x / 4), (x, 0, 2 * pi),
label="lower limit",
rendering_kw={"linestyle": ":"}, show=False)
p1.extend(p2)
p1.extend(p3)
p1.show() On the other hand, I would like to do it like this: from sympy import *
from spb import *
var("x")
plot(
(cos(x + pi / 2) * exp(-x / 4), "oscillator"),
(exp(-x / 4), "upper limit", {"linestyle": ":"}),
(-exp(-x / 4), "upper limit", {"linestyle": ":"}),
(x, 0, 2 * pi),
ylim=(-1.25, 1.25)
) Make no mistake, there is absolutely no way in the entire universe that you'll be able to convince me that the first code block is better than the second, which achieve the same result. But feel free to try. Luckily, this PR enables both ways so you'll be able to use the former, while I'll be able to use the latter, and everyone lives happily ever after. |
My opinion is that this makes the logic much more complicated and have maintainance problem later. For example, if we are going to stick to lists of expressions, it may be better to force people to always pass the list, even for length-1.
However, I don't favor the arguments like this because the IDE poorly gives argument hints and autocompletion. |
return series | ||
|
||
|
||
def _set_labels(series, labels, rendering_kw): |
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.
Why do we have to tie the labels and options to series?
I thought Series should only be implementing the geometry, or at least it could split into something that specifically implements the geometry and styles.
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.
Back-compatibility.
A series is used to generate numerical data and store a few style-related attributes, like label
, or line_color
, or surface_color
inherited from the old implementation, and now the more general rendering_kw
.
After a series has been created, sooner or later it will be processed by a renderer: it is just a function which extract the numerical data and styling options from the series and adds them to the chart.
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.
No. Series
was more simple before #25469, and you started to add a bunch of things there.
Well, I start to believe that label
or line_color
makes sense.
Because line_color
can be similar to 'vertex colors' which is often nontrivial with geometry
And label makes sense in some regards because there is no big deal to consider that as 'name' of a geometry.
But I didn't have a roadmap for that to extend to have much more attributes, if that is not contextually necessary, and if Series
could be a public objects that can be inherited by users.
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 read your comment 3 times, and I still don't understand what you are trying to say?!?!?!?!?!
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.
Don’t be personal
It does not intimidate me.
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.
@sylee957 Please, consider writing in your native language. Then, I'll use Google Translate or something similar. Because right now, I don't understand what kind of grammar you are using. At least 3 out of 4 of your sentences doesn't make any sense to me... It's like reading a text generated from a really old chat-bot: I don't understand you! So, don't take it personal. A communication is successful if two or more people understands each other. I'm unable to understand what you are writing.
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.
Please, consider writing in your native language. Then, I'll use Google Translate or something similar. Because right now, I don't understand what kind of grammar you are using. At least 3 out of 4 of your sentences doesn't make any sense to me... It's like reading a text generated from a really old chat-bot:
I have web archives of your comments
I very often want to plot multiple expressions usually to compare them. The current interface is not very good for doing this though. Often though I might want to overlay different kinds of plot e.g. In general I have to say that I have never been happy with the interface for any plotting library be that matplotlib, Matlab etc. The obscure layers of configuring properties are one issue and then the mix of stateful and declarative things are another. Probably the main problem here is that show(
plot(cos(x + pi / 2) * exp(-x / 4), "oscillator"),
plot_implicit(x**2 + y**2 - 1, "circle"),
xlim=(-2, 2)
) Is there a way to do that? Perhaps we could use different names for these rather than plot(
line(cos(x + pi / 2) * exp(-x / 4), "oscillator"),
line_implicit(x**2 + y**2 - 1, "circle"),
xlim = (-2, 2),
axes = (x, y),
) |
New capabilities are always going to introduce new level of complexities. In my opinion, this is not as "much more" complicated as you would like to think. The real question is: are the new capabilities worth the the complexity? I really think they do!
To be clear, everything you mentioned is also valid for SymPy 1.12: from sympy import *
var("x")
plot((sin(x), (x, 0, 5)), (cos(x), (x, -5, 0)) Here, users can provide arguments of the form All plotting functions into SymPy have this signature: I know nothing about type hinting, but I know that I invested a lot of time to make sure these new capabilities are back-compatible. If I need to type You are free to invest your time in order to improve type hinting on the plotting module. You are clearly more qualified than me. I honestly don't believe that it is possible to achieve what you have in mind while maintaining back-compatibility! |
Right now, p1 = plot(cos(x + pi / 2) * exp(-x / 4), "oscillator", show=False)
p2 = plot_implicit(x**2 + y**2 - 1, "circle", show=False)
(p1+p2).show() It's kind of a PITA to add |
I favor the direction to go
I think that it is best to make plots defined by some 'layer' than composite arguments. |
I am not sure why we have to tell sympy to do something explicity when that is the only way it can be done: so |
The problem is that the style options may likely be very different depending on the types of geometry (2D, 3D, plot_implicit vs plot) |
This break back-compatibility. A new name must be define, maybe Which brings us back to the scope of this PR, extending the arguments to accept label and styling arguments. |
I don't really get what you mean by backward compatibility here. I don't request to delete any new features before. plot with extended tuple arguments looks ugly for me. |
Maybe you can achieve that in a back compatible way, but Just because you don't find the tuple arguments nice, you are willing to add a new mode of operation to an already existing function,
I find them to be extremely nice, compact and friendly to use! I have already provided a very nice example for you to debate, and you purposely avoided it!!!!!!!!! I'm warning you: this is the kind of discussions that enrages me, because you are avoiding to provide counter arguments to the specific problem that this PR addresses, basing exclusively on "tuple arguments looks ugly for me". Let me give you a brief recap: tuple arguments already exists, this PR extends them for good reasons. As for a new plotting interface: open a new issue, collects opinion and start working on that. This PR is part of #23036 , I want to emphasize merging: the development is already completed! |
I am not furious about anything, |
…into merging_plot_10
Ops, sorry guys, I typed the wrong command... |
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
[8059df73] [002bf92c]
<sympy-1.12^0>
- 116±6ms 76.6±1ms 0.66 integrate.TimeIntegrationRisch02.time_doit(10)
- 114±4ms 75.3±2ms 0.66 integrate.TimeIntegrationRisch02.time_doit_risch(10)
- 9.39±0.3ms 5.21±0.2ms 0.56 logic.LogicSuite.time_load_file
- 2.70±0.07ms 817±30μs 0.30 polys.TimePREM_LinearDenseQuadraticGCD.time_op(3, 'sparse')
- 13.8±0.2ms 2.40±0.05ms 0.17 polys.TimePREM_LinearDenseQuadraticGCD.time_op(5, 'sparse')
- 438±10μs 101±3μs 0.23 polys.TimePREM_QuadraticNonMonicGCD.time_op(1, 'sparse')
- 6.78±0.1ms 446±10μs 0.07 polys.TimePREM_QuadraticNonMonicGCD.time_op(3, 'sparse')
- 15.0±0.2ms 1.31±0.01ms 0.09 polys.TimePREM_QuadraticNonMonicGCD.time_op(5, 'sparse')
- 8.15±0.1ms 4.89±0.1ms 0.60 polys.TimeSUBRESULTANTS_LinearDenseQuadraticGCD.time_op(2, 'sparse')
- 35.2±0.9ms 15.1±0.2ms 0.43 polys.TimeSUBRESULTANTS_LinearDenseQuadraticGCD.time_op(3, 'sparse')
- 8.13±0.08ms 1.48±0.03ms 0.18 polys.TimeSUBRESULTANTS_QuadraticNonMonicGCD.time_op(1, 'sparse')
- 19.5±0.6ms 11.6±0.3ms 0.60 polys.TimeSUBRESULTANTS_QuadraticNonMonicGCD.time_op(2, 'sparse')
- 270±4ms 85.6±1ms 0.32 polys.TimeSUBRESULTANTS_QuadraticNonMonicGCD.time_op(3, 'sparse')
- 8.87±0.1ms 677±20μs 0.08 polys.TimeSUBRESULTANTS_SparseGCDHighDegree.time_op(3, 'sparse')
- 37.6±1ms 1.04±0.05ms 0.03 polys.TimeSUBRESULTANTS_SparseGCDHighDegree.time_op(5, 'sparse')
- 775±30μs 270±10μs 0.35 polys.TimeSUBRESULTANTS_SparseNonMonicQuadratic.time_op(1, 'sparse')
- 9.18±0.2ms 289±10μs 0.03 polys.TimeSUBRESULTANTS_SparseNonMonicQuadratic.time_op(3, 'sparse')
- 24.1±1ms 283±4μs 0.01 polys.TimeSUBRESULTANTS_SparseNonMonicQuadratic.time_op(5, 'sparse')
- 221±7μs 128±2μs 0.58 solve.TimeMatrixOperations.time_rref(3, 0)
- 409±10μs 155±3μs 0.38 solve.TimeMatrixOperations.time_rref(4, 0)
- 41.5±2ms 17.4±0.09ms 0.42 solve.TimeSolveLinSys189x49.time_solve_lin_sys
Full benchmark results can be found as artifacts in GitHub Actions |
# single argument: lambda function | ||
f = lambda t: t | ||
p = plot(lambda t: t) | ||
assert isinstance(p[0], LineOver1DRangeSeries) | ||
assert callable(p[0].expr) | ||
assert p[0].ranges[0][1:] == (-10, 10) | ||
assert p[0].get_label(False) == "" | ||
assert p[0].rendering_kw == {} |
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 also notice that lambda function can be used instead of SymPy function.
But I raise another question that if lambda
is used, does it never need SymPy at all at that cod path, or does it still depends on the SymPy.
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.
lambda functions supporting vectorized operations can be used with the plotting module, but don't support all functionalities. Obviously, sympy is required to run the plotting module.
I'm starting to think if it is really not sure that we should acquire the project by merging to SymPy core, |
This happens to me frequently -- I think there is a keyboard shortcut (or an Enter pressed when that action is highlighted on the page). It's annoying to have this happen so undesirably easily. |
If the tuple discussion refers to here I would add this obsevation: In either case you can "store a plot" to be used in another plot. You are either storing them as "p" values or as tuples. p1 = plot(cos(x + pi / 2) * exp(-x / 4), (x, 0, 2 * pi),
label="oscillator",
ylim=(-1.25, 1.25), show=False)
p2 = plot(exp(-x / 4), (x, 0, 2 * pi),
label="upper limit",
rendering_kw={"linestyle": ":"}, show=False)
p3 = plot(-exp(-x / 4), (x, 0, 2 * pi),
label="lower limit",
rendering_kw={"linestyle": ":"}, show=False)
p1.extend(p2)
p1.extend(p3)
p1.show() vs t1 = (cos(x + pi / 2) * exp(-x / 4), "oscillator")
t2 = (exp(-x / 4), "upper limit", {"linestyle": ":"})
t3 = (-exp(-x / 4), "upper limit", {"linestyle": ":"})
plot(t1, t2, t3,
(x, 0, 2 * pi),
ylim=(-1.25, 1.25)
)
|
Yes, default limits will be used. |
@sylee957 , do you have any objection to merging? I know you raised some points but I am not following intently enough to know where you are at with any concerns. |
No, but I would not merge this to petition code-of-conduct violation issues |
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.
What about using the SymPy caps vs lower syntax: |
References to other Issues or PRs
Another PR related to Issue #23036
Brief description of what is fixed or changed
label
keyword argument can now deal with multiple expressions.rendering_kw
. If provided, this dictionary of commands will be passed to the plotting library's function responsible to generate the visualization. It allows to customize the appearance by leveraging the full capabilities of a plotting library.label
andrendering_kw
optional arguments to plotting functions.A couple of new keyword arguments are introduced. This will be properly documented on a future PR.
Back-compatibility is maintained.
Other comments
Up to SymPy 1.12, the
label
keyword argument was unable to set custom labels when plotting multiple expressions with the same function call. The following shows an example of before (up to sympy 1.12) and after (this PR):This PR also extend the number of optional arguments that any plotting function can receive. Up to SymPy 1.12, a generic plotting function had the following form:
plot_func(expr, range1 [tuple, optional], range2 [tuple, optional], ..., **kwargs
)With this PR it is extended to:
plot_func(expr, range1 [tuple, optional], ..., label [str, optional], rendering_kw [dict, optional], **kwargs
)This allows more refinement when plotting and customizing multiple expressions. For example:
Here, a custom label is set on the first expression and custom rendering options are set on the second expression. Note that this plot was generated with spb because sympy's
MatplotlibBackend
is currently unaware of therendering_kw
dictionary: this will be implemented in the next PR.Release Notes