Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

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

Provide feedback

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

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

OptimiseAxes.add_patch to avoid python iteration over bezier segments#28657

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed
tpgillam wants to merge2 commits intomatplotlib:mainfromtpgillam:tg/add_patch_optimisation
Closed

OptimiseAxes.add_patch to avoid python iteration over bezier segments#28657

tpgillam wants to merge2 commits intomatplotlib:mainfromtpgillam:tg/add_patch_optimisation

Conversation

tpgillam
Copy link

@tpgillamtpgillam commentedAug 3, 2024
edited
Loading

PR summary

This aims to optimise calls to_AxesBase.add_patch, where the patch might have a path with many bezier segments. This is done by avoiding a long loop in python, and instead delegating toBBox.update_from_path, which is used when updating limits from other artists.

This provides a ~5x speedup for a call to_AxesBase.add_patch in some of our internal code, with complicated patches with hundreds of bezier segments.

Implementation notes

The implementation is effectively the same as that in_AxesBase._update_line_limits also in this class. It performs the iteration in C++ (in_path.h::update_path_extents).
This is the same approach as used for collections of patches; i.e. the rescaling behaviour is now as performant as:

axes.add_collection(PatchCollection([patch]))

PR checklist

WIP: trying but so far have failed to get the test suite running locally. I followed steps to create necessary venv and build, but runningpytest just hangs indefinitely without consuming any resources. I can however import the local build of matplotlib and perform basic tasks with it by hand.

Copy link

@github-actionsgithub-actionsbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a week or so, please leave a new comment below and that should bring it to our attention. Most of our reviewers are volunteers and sometimes things fall through the cracks.

You can also join uson gitter for real-time discussion.

For details on testing, writing docs, and our review process, please seethe developer guide

We strive to be a welcoming and open project. Please follow ourCode of Conduct.

@jklymak
Copy link
Member

Unfortunately this doesn't passmany tests so I suspect your optimization is returning very different results than the usual code. It may be helpful if you make a test example that is very slow for you so we can understand clearly what you are trying to achieve and then folks may have suggestions.

@tpgillam
Copy link
Author

tpgillam commentedAug 3, 2024
edited
Loading

Indeed, I saw that! I'd like to be able to see what the differences are locally; but it seems that I can't even run one by passing a specific path topytest. (It hangs with zero output and strace tells me it's just in an infinite loop ofwait4 andpselect6 calls - never come across this before!)

My suspicion is that the difference might be to do with a simple consideration of control points vs extremal values on the curve; but very hard to say without being able to see the output.

@tpgillam
Copy link
Author

tpgillam commentedAug 3, 2024
edited
Loading

Here is a silly example, but it's minimal whilst demonstrating the issue:

importtimeitimportnumpyfrommatplotlibimportpyplotfrommatplotlib.axesimportAxesfrommatplotlib.collectionsimportPatchCollectionfrommatplotlib.patchesimportPolygonpoints=numpy.random.rand(10000,2)patch=Polygon(points)_,axes1=pyplot.subplots()assertisinstance(axes1,Axes)t1=timeit.timeit(lambda:axes1.add_patch(patch),number=1)_,axes2=pyplot.subplots()assertisinstance(axes2,Axes)t2=timeit.timeit(lambda:axes2.add_collection(PatchCollection([patch],match_original=True)),number=1,)print(t1)print(t2)
0.104658107971772550.000516503001563251

The difference in this case is nearly a factor of 200x on my machine. In a more realistic case where also considering figure construction then it's less significant, but still noticeable.

Running the first one through the profiler shows the time being spent in_update_patch_limits.

         240329 function calls (240324 primitive calls) in 0.148 seconds   Ordered by: cumulative time   List reduced from 138 to 40 due to restriction <40>   ncalls  tottime  percall  cumtime  percall filename:lineno(function)        1    0.000    0.000    0.148    0.148 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/axes/_base.py:2406(add_patch)        1    0.016    0.016    0.148    0.148 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/axes/_base.py:2419(_update_patch_limits)    10002    0.010    0.000    0.066    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/path.py:413(iter_bezier)    10001    0.029    0.000    0.052    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/bezier.py:208(__call__)    10001    0.033    0.000    0.047    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/bezier.py:199(__init__)    20002    0.020    0.000    0.020    0.000 {method 'outer' of 'numpy.ufunc' objects}    10001    0.008    0.000    0.011    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/bezier.py:203(<listcomp>)    10001    0.005    0.000    0.010    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/bezier.py:283(axis_aligned_extrema)    30003    0.009    0.000    0.009    0.000 {built-in method numpy.array}        1    0.001    0.001    0.004    0.004 /somewhere/.venv/lib/python3.11/site-packages/numpy/core/shape_base.py:219(vstack)    10002    0.003    0.000    0.004    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/path.py:347(iter_segments)    10001    0.003    0.000    0.003    0.000 {built-in method numpy.arange}    20008    0.003    0.000    0.003    0.000 {built-in method numpy.asarray}    60003    0.002    0.000    0.002    0.000 {built-in method math.factorial}        1    0.002    0.002    0.002    0.002 /somewhere/.venv/lib/python3.11/site-packages/numpy/core/shape_base.py:81(atleast_2d)    20003    0.001    0.000    0.001    0.000 {method 'append' of 'list' objects}    10001    0.001    0.000    0.001    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/bezier.py:242(degree)    10002    0.000    0.000    0.000    0.000 {built-in method numpy.asanyarray}        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/path.py:470(cleaned)        1    0.000    0.000    0.000    0.000 {built-in method matplotlib._path.cleanup_path}        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/axes/_base.py:2521(update_datalim)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:931(update_from_data_xy)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:859(update_from_path)        1    0.000    0.000    0.000    0.000 {built-in method matplotlib._path.update_path_extents}        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/artist.py:769(set_clip_path)        2    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/patches.py:306(get_transform)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:1428(__sub__)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/patches.py:790(get_patch_transform)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/numpy/core/shape_base.py:215(_vhstack_dispatcher)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/numpy/core/shape_base.py:207(_arrays_for_stack_dispatcher)      3/2    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:2394(__eq__)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:1720(__eq__)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:2172(__eq__)      3/2    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:1787(__eq__)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/path.py:99(__init__)        4    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:1350(__add__)        4    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:2517(composite_transform_factory)        2    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:2401(_iter_break_from_left_to_right)        3    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/transforms.py:2358(__init__)        1    0.000    0.000    0.000    0.000 /somewhere/.venv/lib/python3.11/site-packages/matplotlib/patches.py:924(get_bbox)

In our real use we're somewhat performance-sensitive when drawing fairly complex patches. So removing this bottleneck by pushing the iteration down to C++ would be helpful. Myhope was that adding aPatchCollection would have the same data-limit-adjusting properties as adding the patches individually; and if this were true then my aim with this PR was to mimic this withinadd_patch.

@jklymak
Copy link
Member

The actions have an "Upload Artifact " step with a link to the artifacts. No idea why your pytest is not working. I would make sure it is running from the same python as your env.

@tpgillam
Copy link
Author

No idea why your pytest is not working.

Finally got a python stack trace after interruptingpytest --help that shows it hanging inpyvirtualdisplay.abstractdisplay::_wait_for_pipe_text. I'm on a work machine running WSL at the moment, seems plausible that this won't be helping.

@jklymak
Copy link
Member

jklymak commentedAug 3, 2024
edited
Loading

You can use blame to see that this code came in relatively recently:#19124 and#24177. At the least if you remove this code I'd expecttest_bezier_autoscale andtest_small_autoscale to definitely fail. If you are to remove it, then you'd want an argument why the fixes in those PRs are not worth the performance hit. Unfortunately, you can usually only have two of easy, quick, or good. We are probably going to err on the side of easy & good, rather than easy & quick.

Of course you also seem to have removed extent finding all together as some of the changes are not trivial in the rest of the test suite.

@tpgillam
Copy link
Author

tpgillam commentedAug 3, 2024
edited
Loading

My underlying assumption here was that the limit adjustment for a collection of one patch would be the same as for that one patch alone. Ithink my understanding from these failures (and a few inferences about the code) is that this assumption is false, although without the ability to iterate on the tests locally I'm not 100% sure.

Anyway, I'll close this PR. I'm not advocating for changing behaviour; it just appeared at a glance that this might be behaviour preserving, but never mind!

@tacaswell
Copy link
Member

If you have very large paths with lots of bezier curves, it might be worth turning off autoscaling and handling setting the limits your self.

@tpgillam
Copy link
Author

If you have very large paths with lots of bezier curves, it might be worth turning off autoscaling and handling setting the limits your self.

Thanks for the suggestion. Unfortunately callingaxes.autoscale(False) first doesn't make any difference. (More generally, I can't see any user-configurable way to prevent the call to_iter_bezier when callingadd_patch. The data limits are always updated, regardless of the autoscale settings on the axes themselves.)

@tacaswell
Copy link
Member

hmm, looking at the code it looks like we always compute the data limits and only sometimes update the view limits.

I can see a case for making "update the data limits" lazy and computing it on the demand, but I do not think that would be a small project to do.

tpgillam reacted with thumbs up emoji

@tacaswell
Copy link
Member

Sorry for pointing you down a dead-end.

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@github-actionsgithub-actions[bot]github-actions[bot] left review comments

Assignees
No one assigned
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

3 participants
@tpgillam@jklymak@tacaswell

[8]ページ先頭

©2009-2025 Movatter.jp