-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Improve handling of degenerate jacobians in non-rectilinear grids. #24714
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
83a5aad
to
1ea703e
Compare
1ea703e
to
e4ef1cd
Compare
cd3e502
to
8fd131a
Compare
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 isn't the easiest to follow all of the ps and qs around in the code, is there a reason to use p/q instead of u/v as in your added docstring?
p and q are in the same space as x and y (either p is along x and q is along y, or the other way round: we compute the derivatives for both orientations), whereas u and v are in the image space of f. |
8fd131a
to
3c6efe2
Compare
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.
Take or leave the second suggested refactoring.
xlo, xhi = sorted(xlim) | ||
xdlo = xs - xlo | ||
xdhi = xhi - xs | ||
xeps_max = np.maximum(xdlo, xdhi) | ||
xeps0 = np.where(xdhi >= xdlo, 1, -1) * np.minimum(eps0, xeps_max) |
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.
Also optional, but IMHO it would make sense to extract a method here as well. Essentially this is
xeps_max, xeps0 = func(xlim, xs)
. Everything else is just temporary variables. and the same is repeated for y.
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.
Sure.
grid_helper_curvelinear and floating_axes have code to specifically handle the case where the transform from rectlinear to non-rectilinear axes has null derivatives in one of the directions, inferring the angle of the jacobian from the derivative in the other direction. (This angle defines the rotation applied to axis labels, ticks, and tick labels.) This approach, however, is insufficient if the derivatives in both directions are zero. A classical example is e.g. the ``exp(-1/x**2)`` transform, for which all derivatives are zero. To handle this case more robustly (and also to better encapsulate the angle calculation, which is currently repeated at a few places), instead, one can increase the step size of the numerical differentiation until the gradient becomes nonzero. This amounts to moving along the corresponding gridline until one actually leaves the position of the tick, and thus is indeed a justifiable approach to compute the tick rotation. Full examples: import numpy as np from matplotlib import pyplot as plt from matplotlib.projections.polar import PolarTransform from matplotlib.transforms import Affine2D from mpl_toolkits.axisartist import ( angle_helper, GridHelperCurveLinear, HostAxes) import mpl_toolkits.axisartist.floating_axes as floating_axes # def tr(x, y): return x - y, x + y # def inv_tr(u, v): return (u + v) / 2, (v - u) / 2 @np.errstate(divide="ignore") # at x=0, exp(-1/x**2)=0; div-by-zero can be ignored. def tr(x, y): return np.exp(-x**-2) - np.exp(-y**-2), np.exp(-x**-2) + np.exp(-y**-2) def inv_tr(u, v): return (-np.log((u+v)/2))**(1/2), (-np.log((v-u)/2))**(1/2) plt.subplot( 121, axes_class=floating_axes.FloatingAxes, grid_helper=floating_axes.GridHelperCurveLinear( (tr, inv_tr), extremes=(0, 10, 0, 10))) ax = plt.subplot( 122, axes_class=HostAxes, grid_helper=GridHelperCurveLinear( Affine2D().scale(np.pi / 180, 1) + PolarTransform() + Affine2D().scale(2, 1), extreme_finder=angle_helper.ExtremeFinderCycle( 20, 20, lon_cycle=360, lat_cycle=None, lon_minmax=None, lat_minmax=(0, np.inf), ), grid_locator1=angle_helper.LocatorDMS(12), tick_formatter1=angle_helper.FormatterDMS(), ), aspect=1, xlim=(-5, 12), ylim=(-5, 10)) ax.axis["lat"] = axis = ax.new_floating_axis(0, 40) axis.label.set_text(r"$\theta = 40^{\circ}$") axis.label.set_visible(True) ax.grid(True) plt.show()
3c6efe2
to
5ca9137
Compare
grid_helper_curvelinear and floating_axes have code to specifically handle the case where the transform from rectlinear to non-rectilinear axes has null derivatives in one of the directions, inferring the angle of the jacobian from the derivative in the other direction. (This angle defines the rotation applied to axis labels, ticks, and tick labels.)
This approach, however, is insufficient if the derivatives in both directions are zero. A classical example is e.g. the
exp(-1/x**2)
transform, for which all derivatives are zero. To handle this case more robustly (and also to better encapsulate the angle calculation, which is currently repeated at a few places), instead, one can increase the step size of the numerical differentiation until the gradient becomes nonzero. This amounts to moving along the corresponding gridline until one actually leaves the position of the tick, and thus is indeed a justifiable approach to compute the tick rotation.Full example:
np.broadcast_shapes requires numpy 1.20, which is allowed per NEP29.
before:


after:
(note the orientation of the ticks at (0, 0)).
Followup to #24509.
Edit: Currently fails because this doesn't support a tick at r=0 on an r axis in a polar plot anymore, although I guess the old approach wasn't really robust anyways (e.g. it would probably fail on an r axis with a "sheared" theta...). To be further investigated... For the record, a case where the old approach also failed:
Note that the tick at r=0 on the theta=40° axis is not parallel with the other ticks on the same axis, because locally there's no gridline that can be followed and so the old code just falls back to drawing the tick locally perpendicular to the axis. I guess the right approach I would like to try is to see whether one can instead also slightly move along the r axis and take the limit as r->0 of the tick angle (because that tick angle is actually constant for all r!=0, so taking the limit will give the right answer.
PR Summary
PR Checklist
Documentation and Tests
pytest
passes)Release Notes
.. versionadded::
directive in the docstring and documented indoc/users/next_whats_new/
.. versionchanged::
directive in the docstring and documented indoc/api/next_api_changes/
next_whats_new/README.rst
ornext_api_changes/README.rst