Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 1 commit into from
Dec 19, 2024

Conversation

anntzer
Copy link
Contributor

@anntzer anntzer commented Dec 13, 2022

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:

import matplotlib.pyplot as plt
import mpl_toolkits.axisartist.floating_axes as floating_axes
import numpy as np

# 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)

ax1 = plt.figure().add_subplot(
    axes_class=floating_axes.FloatingAxes,
    grid_helper=floating_axes.GridHelperCurveLinear(
        (tr, inv_tr), extremes=(0, 10, 0, 10)))

plt.show()

np.broadcast_shapes requires numpy 1.20, which is allowed per NEP29.

before:
old
after:
new
(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:

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)


ax1 = plt.figure().add_subplot(
    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))
ax1.axis["lat"] = axis = ax1.new_floating_axis(0, 40)
axis.label.set_text(r"$\theta = 40^{\circ}$")
axis.label.set_visible(True)
ax1.grid(True)

plt.show()

test
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

  • Has pytest style unit tests (and pytest passes)
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • New plotting related features are documented with examples.

Release Notes

  • New features are marked with a .. versionadded:: directive in the docstring and documented in doc/users/next_whats_new/
  • API changes are marked with a .. versionchanged:: directive in the docstring and documented in doc/api/next_api_changes/
  • Release notes conform with instructions in next_whats_new/README.rst or next_api_changes/README.rst

@anntzer anntzer marked this pull request as draft December 13, 2022 20:48
@anntzer anntzer force-pushed the jacobian_angle branch 2 times, most recently from 83a5aad to 1ea703e Compare December 13, 2022 20:57
@anntzer anntzer marked this pull request as ready for review December 13, 2022 20:57
@anntzer anntzer changed the title Improve handling of degenerate jacobians in non-rectiliear grids. Improve handling of degenerate jacobians in non-rectilinear grids. Dec 13, 2022
@anntzer anntzer marked this pull request as draft December 13, 2022 21:43
@anntzer
Copy link
Contributor Author

anntzer commented Apr 20, 2023

Fixed; now see that

  1. on the left, the zero ticks are properly oriented,
  2. on the right, the r=0 tick is parallel to the other radial axis ticks (this is done by detecting that there is no gridline here and thus moving a tiny bit along the orthogonal direction, i.e. the radial axis, and redoing the walk-along-gridline computation).
    test

@anntzer anntzer marked this pull request as ready for review May 17, 2023 13:27
@anntzer
Copy link
Contributor Author

anntzer commented Dec 9, 2024

Kindly bumping. This is touching slightly obscure parts of the library, but fixing real bugs, as shown by the reproducer below:

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, floating_axes, GridHelperCurveLinear, HostAxes)


@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)


fig = plt.figure()

ax1 = fig.add_subplot(
    121,
    axes_class=floating_axes.FloatingAxes,
    grid_helper=floating_axes.GridHelperCurveLinear(
        (tr, inv_tr), extremes=(0, 10, 0, 10)))
ax1.axis["bottom"].major_ticks.set_tick_out(True)
ax1.axis["left"].major_ticks.set_tick_out(True)
ax1.grid(True)

ax2 = fig.add_subplot(
    122,
    axes_class=HostAxes,
    grid_helper=GridHelperCurveLinear(
        Affine2D().scale(np.pi / 180, 1)
        + PolarTransform(apply_theta_transforms=False)
        + Affine2D().scale(3, 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))
ax2.axis["lat"] = ax2.new_floating_axis(0, 40)
ax2.grid(True)

plt.show()

before:
old
after:
new
(Compare the orientation of the ticks at (0, 0).)

Copy link
Contributor

@greglucas greglucas left a 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?

@anntzer
Copy link
Contributor Author

anntzer commented Dec 14, 2024

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.

Copy link
Member

@timhoffm timhoffm left a 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.

Comment on lines 55 to 59
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)
Copy link
Member

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.

Copy link
Contributor Author

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()
@QuLogic QuLogic merged commit 05fc1b3 into matplotlib:main Dec 19, 2024
39 checks passed
@QuLogic QuLogic added this to the v3.11.0 milestone Dec 19, 2024
@anntzer anntzer deleted the jacobian_angle branch December 20, 2024 09:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants