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

MNT: Don't require renderer for window_extent and tightbbox#22745

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

Merged
greglucas merged 1 commit intomatplotlib:mainfromjklymak:mnt-cache-renderer
Jun 3, 2022

Conversation

jklymak
Copy link
Member

@jklymakjklymak commentedMar 31, 2022
edited
Loading

PR Summary

This PR is a proof of concept of making renderers globally optional. We don't need to use this intight_layout because it has a funky_draw_disabled context. However, constrained_layout can have all the renderer tracking pulled out and still works in the tests and interactively so far as I can tell.

See#22744 for more discussion, but I think deciding on the renderer at the Figure level is the right thing to do, and stop trying to sort it out at a higher level.

See also:

PR Checklist

Tests and Styling

  • Has pytest style unit tests (andpytest passes).
  • IsFlake 8 compliant (installflake8-docstrings and runflake8 --docstring-convention=all).

Documentation

  • New features are documented, with examples if plot related.
  • New features have an entry indoc/users/next_whats_new/ (follow instructions in README.rst there).
  • API changes documented indoc/api/next_api_changes/ (follow instructions in README.rst there).
  • Documentation is sphinx and numpydoc compliant (the docs shouldbuild without error).

@tacaswelltacaswell added this to thev3.6.0 milestoneMar 31, 2022
@jklymakjklymak marked this pull request as ready for reviewMarch 31, 2022 20:12
@jklymakjklymak mentioned this pull requestApr 5, 2022
6 tasks
@@ -2430,6 +2436,14 @@ def axes(self):

get_axes = axes.fget

def _get_cached_renderer(self):
Copy link
MemberAuthor

Choose a reason for hiding this comment

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

This is the main new (private) method. Users should generally not need to fetch the renderer going forward...

Copy link
Contributor

Choose a reason for hiding this comment

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

I find the naming a bit confusing. I'd suggest just_get_renderer(self), the caching should be an implementation detail. Theif self.cachedRenderer is not None seems like it should be implemented in theself.canvas.get_renderer() method (and indeed, there is some logic to re-use the Agg renderer for this)

defget_renderer(self,cleared=False):
w,h=self.figure.bbox.size
key=w,h,self.figure.dpi
reuse_renderer= (hasattr(self,"renderer")
andgetattr(self,"_lastKey",None)==key)
ifnotreuse_renderer:
self.renderer=RendererAgg(w,h,self.figure.dpi)
self._lastKey=key
elifcleared:
self.renderer.clear()
returnself.renderer

@jklymak
Copy link
MemberAuthor

Hmm, for some reason.Artist.get_tightlayout doesn't resolve..Artist.get_window_extent does no problem.

@jklymakjklymak added the topic: geometry managerLayoutEngine, Constrained layout, Tight layout labelApr 8, 2022
@jklymakjklymak changed the titleMNT: proof of concept cache rendererMNT: Don't require renderer for window_extent and tightbboxApr 8, 2022
@tacaswell
Copy link
Member

I think that for internal calls, if we have the renderer we should pass it through rather than relying on the cache to implicitly pass it to something deep in the call stack if it is needed.

@tacaswelltacaswell modified the milestones:v3.6.0,v3.7.0Apr 30, 2022
@jklymak
Copy link
MemberAuthor

I think that for internal calls, if we have the renderer we should pass it through rather than relying on the cache to implicitly pass it to something deep in the call stack if it is needed.

Maybe? However, a lot of the time we are only fetching the renderer to pass it through.

@greglucas
Copy link
Contributor

Would it be possible to usefig.canvas.get_renderer() everywhere? It seems like theget_renderer() call should handle the caching if it can.

@jklymak
Copy link
MemberAuthor

@greglucas I think that's what the newfig._get_cached_renderer does (if it needs to)?

The point here is that all the artists know what their figure is, so if they need the renderer, they can just ask their figure for it. However, it seems a considerable simplification not to ask users to get the renderer.

Copy link
Contributor

@greglucasgreglucas left a comment

Choose a reason for hiding this comment

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

Overall I think this is a nice simplification for users to have the renderer sorted out "under the hood" for them rather than having to explicitly pass in a renderer. Just a few comments wondering about where the lines are drawn for when the renderer gets requested (get_tightbbox, window_extent, somewhere else...) and which object/method's job it is to keep track of the caching.

@@ -199,15 +199,7 @@ def auto_adjust_subplotpars(


def get_renderer(fig):
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you remove this entirely? It looks like this is only used withinLayoutEngine now, so could be replaced there.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Yep

Comment on lines 4480 to 4481
if renderer is None:
renderer = self.figure._get_cached_renderer()
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to put this insideget_tightbbox() versusget_window_extent()? It looks like further down you put it inside theget_window_extent() call, so I wasn't following where this is needed versus whereNone could be passed through...

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

The locator needs the renderer, however, its debatable whether we should remove that as well.

@@ -2430,6 +2436,14 @@ def axes(self):

get_axes = axes.fget

def _get_cached_renderer(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

I find the naming a bit confusing. I'd suggest just_get_renderer(self), the caching should be an implementation detail. Theif self.cachedRenderer is not None seems like it should be implemented in theself.canvas.get_renderer() method (and indeed, there is some logic to re-use the Agg renderer for this)

defget_renderer(self,cleared=False):
w,h=self.figure.bbox.size
key=w,h,self.figure.dpi
reuse_renderer= (hasattr(self,"renderer")
andgetattr(self,"_lastKey",None)==key)
ifnotreuse_renderer:
self.renderer=RendererAgg(w,h,self.figure.dpi)
self._lastKey=key
elifcleared:
self.renderer.clear()
returnself.renderer

@jklymak
Copy link
MemberAuthor

Changed the name to_get_renderer

@jklymak
Copy link
MemberAuthor

Marking as "Needs discussion" because I think there are ideas to go the other way here and make Artists more independent of the figure. I guess if that is really going to happen, then we should think about this. But I'm unconvinced the piping necessary to make Artists portable between figures is worth the considerable complication.

@jklymakjklymak added the status: needs comment/discussionneeds consensus on next step labelMay 10, 2022
@jklymak
Copy link
MemberAuthor

@tacaswell I doubt I can attend the weekly meeting (travelling), but feel free to discuss this and decide whether we think we willreally ever disentangle artists from the figure that owns them.

@anntzer
Copy link
Contributor

anntzer commentedMay 12, 2022
edited
Loading

FWIW, note that right now some artists can end up being drawn without knowing who their parent figure is, e.g. thePathClippedImagePatch.bbox_image attribute in thedemo_text_path.py example (try printingp.bbox_image.figure at the end of the script, for example).

Perhaps we may consider that a bug and state that if you set up artists manually, it is your responsibility to make sure that they know their parent figure, but that would probably need to be documented.

@jklymak
Copy link
MemberAuthor

I guess I'd consider it a bad idea that when we add them to the figure they do not get the figure info added to them. But we can do that at the add_artist time.

@jklymak
Copy link
MemberAuthor

FWIW, note that right now some artists can end up being drawn without knowing who their parent figure is, e.g. thePathClippedImagePatch.bbox_image attribute in thedemo_text_path.py example (try printingp.bbox_image.figure at the end of the script, for example).

Perhaps we may consider that a bug and state that if you set up artists manually, it is your responsibility to make sure that they know their parent figure, but that would probably need to be documented.

Following up on this, yes, I think this is a bug. The example even says that the results are dpi dependent - I don't understand how this is even supposed to work if the artist doesn't know its figure.

@anntzer
Copy link
Contributor

Sure. If you don't want to fix that example here, can you open a separate issue to track that problem? I think we also need to state explicitly somewhere in the docs (and changelog) that artists must have their .figure attribute set.

@jklymak
Copy link
MemberAuthor

I take it back about that being a bug. Whether an artist has a parent figure or not depends on the artists, as you point out. This PR doesn't really worry about that - it is not trying to force all artists to not require a renderer for their get_window_extent, its just trying to make the argument optional for those artists that already have enough info to not need the renderer.

In the example you provided of BboxImage, it requires a renderer, and indeed in the fallback in BboxImage doesn't work (it callsget_figure() which returnsNone). I imagine the same problem occurs for any Artist not directly added to an Axes. I don't think this PR makes the situation any worse (you can still always pass a renderer to get_window_extent), and still simplifies things for most artists.

Note that the layout managers, where the extents are mostly used internally, won't drill down beyond the artists in the axes. Downstream libraries don't need to use this simplification if they don't want (they can continue to pass the renderer). The only disadvantage is a bit of inconsistency for users knowing whether an artist needs the renderer passed or not. However, most folks asking for the bbox are presumably doing something semi complicated, and should be able to handle the inconsistency (or just always pass renderer if in doubt).

@tacaswell
Copy link
Member

My understanding of how we got here is:

  • some operations on someArtists do indeed require a live renderer. However not all artists actually need the renderer
  • in some of those methods the renderer was required and in others it was optional and tried to grab a cached one from someplace
  • this centralizes all of the caches in one place and makes the renderer optional all the places

I think that the source of the bugs here was thatanything was accessing a cached renderer and my instinct would be to go the other way and try to remove the cache ("There are are two hard problems in computer science: naming things, cache invalidation, and off-by-one bugs"). However, it is probably too late for that (as we have long had the cache and it is useful even if it gives the users a foot-cannon) so centralizing it is a net good, however we should view this cache as for the users not for us to use internally. I think it is much better forusers to use our cache rather than keep their own cached renderer (which is definitely going to go out of sync because we do not know about it to invalidate it).

One thing done in this PR with I'm very 👎🏻 on is to then rely on this central cache for internal operations. I think it bakes too many assumption that a (should be optional) cache isdefinitely there and that the reverse connections up the Artist tree are there. I am also concerned that this is a private method so that down-stream library should probably not use it.

On a bit of a philosophical note, mpl as three layers the chrome on top (plt.plot /ax.plot /fig.subplots() / etc ) that are basicallyArtist factories, a middle layer which is theArtist instances which holdbuckets of state, and then thedraw method + renderers that produce an output that someone outside of mpl knows what to do with. Part of the state theArtsits hold is the actual data, part of the state is how to visualize it (which we specify via the class of the instance), part of the state is any "styling" like color, line style, ... (which really should be thought of as data but for another day), part of the state is some intermediate transformation (e.g. norms and colormaps for ScalarMappables), part of it is the "data to display" transformations, and part of it is a hierarchy of Artists. Many (if not all) of our difficulties come from the fact that we tend to mix up all of these kinds of state and that various parts of the library "know too much" about other parts. Because they know too much we eventually start to rely on that knowledge which leads to very brittle cross connections in the code base.

I think the medium term program we are currently engaging in is to start to tease apart all of this state into parts that are clearer and easier to reason about. One place where we get ourselves into consistent trouble is that we have lots of circular references:Figures know about theirAxess andAxess know whatFigure they are in and so on through the entire Artist tree. However, I am not 100% sure that we actually need the back references! Taking a look at where else we actually useArtist.figure it is mostly in the picking logic and accessing the figure transforms / dpi. It does not look trivial to eliminate the back reference right now, but it at least looks plausible. I would very much prefer that we do not further bake in the that these back connections exist (which, fair, they do) when we have an option not to.


I think that we should not change the signature of any of the methods, but allowNone to be passed in to mean "go get the cached one if you can". I am slightly skeptical of this step being able to create and cache a renderer andvery skeptical that we want to be relying on the cache when we have access to the actual renderer already.


The arguemnet here is is it "simpler" to do

deffoo(obj):a=obj.a    ....my_obj.a='a'foo(my_obj)

or

deffoo(obj,a):    ...foo(my_obj,'a')

I would argue that in cases where.a exists only to smuggle inputs intofoo (rather than being something inherent about the data model ofobj) then the second case is clearer and more explicit.

In this case I have the position thatArtist.figure is not an inherent part of theArtist and failing that, a cached renderer is not an inherent part of what aFigure is, thus anyplace we need a renderer, it should be required in the signature (with an option of "do what I mean" API of takingNone for users to opt-into caching).

Copy link
Member

@tacaswelltacaswell left a comment

Choose a reason for hiding this comment

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

I left a long essay. Anyone can dismiss this.

@jklymak
Copy link
MemberAuthor

Thanks@tacaswell for your thoughts.

This PR does two things:

  1. makesrenderer and optional kwarg for all ofget_tightbbox andget_window_extent calls in the library. If not passed, we try and access the cache, if the renderer is needed at all (its usually is not). My read is that you are OK with this as a public interface?
  2. it removes the renderer calls in_constrained_layout and_tight_layout. I don't see that any other internal code is changed. I can easily back this part out, I just was considering it as eating our own dogfood, rather than an important part of the PR. It also simplifies snaking renderer through a bunch of calls, but that really doesn't matter a lot, and indeed is maybe microscopically faster to only call get_renderer once.

So far as we can tell, we only explicitly needrenderer for text items, we have no idea how big they are until we render them.

WRT whether we should stashfigure on artists, so far as I can tell we already stash figure information on artists via thetransform attribute. I think that as long as we do that, we should also explicitly add the figure. In general, I think trees are much easier to use if children know their parents. If you need to prune a child and graft to another tree, that's fine, but in general it usually makes life a lot easier if you can walk up a tree as well as down it.Artist.set_figure has been part of the library for at least 18 years, and I'm not clear what the practical advantage of backing it out would be.

@jklymak
Copy link
MemberAuthor

Latest commit reverts the changes in_constrained_layout andcontour.py where the new version did not explicitly pass in the renderer. Those were the only internal uses I think the old version was changing.

@anntzer
Copy link
Contributor

anntzer commentedMay 21, 2022
edited
Loading

FWIW I have been wanting to make renderer optional for a long time too (because threading it through layers and layers of calls just so that texts can know their extents is a bit annoying). While I also agree with@tacaswell's point that cache invalidation is tricky, note that the question here is about the invalidation offigure._cachedRenderer; it doesn't really matter, IMO, whetherartist.get_window_extent() fetches the renderer fromartist.figure or whether we pass the renderer to it explicitly (because if we're doing this in a non-draw context, we most likely are layouting some parent artist (ultimately the toplevel figure) and are grabbing the renderer fromfigure._cachedRenderer anyways)? IOW, unless we carefully start avoiding refering to artist.figure, we're not really making the cache problem worse?

"""Return lists of bboxes for ticks' label1's and label2's."""
if renderer is None:
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you need to normalize here; just let the labels do it?

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

That depends not he strategy we want to employ - it could wait to normalize until the end, but this does save a bunch of calls to_get_renderer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair.

@jklymakjklymak requested a review fromtacaswellJune 1, 2022 11:44
@jklymak
Copy link
MemberAuthor

@tacaswell, can you reassess now that the internal uses have been cleared up. Note that this also cleans up our internal definition of the renderer to one location.

Copy link
Member

@tacaswelltacaswell left a comment

Choose a reason for hiding this comment

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

I am on board with this 👍🏻

@greglucasgreglucas merged commit0d433b4 intomatplotlib:mainJun 3, 2022
@QuLogicQuLogic modified the milestones:v3.7.0,v3.6.0Jun 14, 2022
@QuLogicQuLogic removed the status: needs comment/discussionneeds consensus on next step labelJun 14, 2022
@jklymakjklymak deleted the mnt-cache-renderer branchOctober 30, 2022 16:30
@QuLogicQuLogic mentioned this pull requestMar 9, 2023
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@anntzeranntzeranntzer left review comments

@tacaswelltacaswelltacaswell approved these changes

@greglucasgreglucasgreglucas approved these changes

Assignees
No one assigned
Labels
topic: geometry managerLayoutEngine, Constrained layout, Tight layout
Projects
None yet
Milestone
v3.6.0
Development

Successfully merging this pull request may close these issues.

5 participants
@jklymak@tacaswell@greglucas@anntzer@QuLogic

[8]ページ先頭

©2009-2025 Movatter.jp