GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Cleanup 1D case in transform_affine() by iccir · Pull Request #32547 · matplotlib/matplotlib

Conversation

@iccir

@iccir iccir commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

PR summary

This PR cleans up the 1D case of Affine2DBase.transform_affine.

  • Keeps 1D pathway in Python Land. Performing a bit of math on the Python side is faster than the multiple conversions needed to get two floats into affine_transform_1d.
  • Removes affine_transform_1d in C++ land and associated code in the wrapper.
  • Changes std::runtime_exception to std::invalid_argument. This aligns all exception types and results in a cleaner test_invalid_transform.
  • Fixes a type issue: set_matrix() takes an ArrayLike, get_matrix() returns a np.ndarray. You can call set_matrix() with an ArrayLike and receive the original un-numpified array. There may be a performance gain here as well.

I used the following benchmark to measure performance:

affinebench.py
import random
import time
import timeit

import numpy as np
from matplotlib.transforms import Affine2D

transform = Affine2D().scale(2.0).translate(10.0, 20.0)

rng = np.random.default_rng(42)
points = [(random.uniform(-1, 1), random.uniform(-1, 1)) for _ in range(100_000)]

total_time = timeit.timeit(
    lambda: [transform.transform_affine(point) for point in points],
    number=1,
)

print(f"Total time: {total_time * 1000:.3f} ms")

On my machine, total time goes from ~310ms to ~82ms.

AI Disclosure

  • I generally use AI as a search engine.
  • I had AI make the attached "affinebench.py" file and help with the test case code.
  • All other code is mine.

PR quality check

  • Use an expressive title, e.g. "Fix title font property precedence"
  • New and changed code is tested
  • [/] Plotting related features are demonstrated in an example
  • [/] New features and API changes have release notes
  • Documentation complies with general and docstring guidelines

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@iccir
Cleanup 1D case in transform_affine()
4167dde
@github-actions github-actions Bot added topic: path handling topic: transforms and scales labels Oct 27, 2026
@iccir

iccir commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

An alternate implementation is:

    def _transform_affine(self, values):
        # values may either be a 1D sequence containing a single vertex (`(x, y)`)
        # or a 2D sequence of vertices. For best performance, transform the single
        # vertex in Python and delegate the 2D sequence to the `_path` module.
        values = np.asarray(values, float, "C")
        mtx = self.get_matrix()
        if values.ndim == 1 and values.size == 2:
            return (mtx @ np.append(values, 1))[:-1]
        else:
            return affine_transform(values, mtx)

That's still faster than the original (~310ms down to ~190ms) but 2x slower compared to the optimized version in this PR. It is more readable, however.

I don't have strong opinions either way - my main goal with this PR is to reduce the complexity of Py_affine_transform() in _path to help with the nanobind transition.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@iccir iccir mentioned this pull request Oct 30, 2026
nanobind Umbrella #32560
Open
@scottshambaugh scottshambaugh added the Performance label Oct 30, 2026
scottshambaugh
scottshambaugh approved these changes Oct 30, 2026
View reviewed changes

@scottshambaugh scottshambaugh left a comment

Copy link
Copy Markdown
Contributor

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.

This looks perfectly readable to me! I'd take the performance over your alternative

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
timhoffm
timhoffm reviewed Oct 30, 2026
View reviewed changes
Comment thread lib/matplotlib/transforms.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread lib/matplotlib/transforms.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@iccir @timhoffm
Update lib/matplotlib/transforms.py
30cf0fd
Co-authored-by: Tim Hoffmann <[email protected]>
timhoffm
timhoffm approved these changes Oct 31, 2026
View reviewed changes

@timhoffm timhoffm left a comment

Copy link
Copy Markdown
Member

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.

Can't check easily on the phone: Is the new scalar branch covered in the tests?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@iccir

iccir commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Can't check easily on the phone: Is the new scalar branch covered in the tests?

Yes, Codecov is showing all lines in green.

The if DEBUG: branch where we try to warn against poor performance isn't hit by the test suite (it wasn't before). I'll double-check that manually.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@iccir

iccir commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

I changed DEBUG to True and verified that the warning still prints. Everything else is hit by the test suite.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@timhoffm

timhoffm commented Oct 31, 2026

Copy link
Copy Markdown
Member

“Being hit in a test” is not necessarily the same as being tested - though I suspect that some image comparison would fail.

Should we nevertheless add a test verifying that (a, b) and [(a, b)] would yield the same result? I assume they should run through different code paths.

iccir reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@iccir

iccir commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Should we nevertheless add a test verifying that (a, b) and [(a, b)] would yield the same result? I assume they should run through different code paths.

Now that I think about it, I think we should go further. TestAffine2D has several tests that follow a pattern:

assert_array_almost_equal(trans.transform(self.single_point), [-3, -2])
assert_array_almost_equal(trans.transform(self.multiple_points),
    [[-6, -0], [-9, -6], [0, -8]])
…
assert_array_almost_equal(trans.transform(self.single_point), [-0.5, 0.75])
assert_array_almost_equal(trans.transform(self.multiple_points),
    [[-2, -0.5], [-1.5, 2.25], [2, 4]])

I think we should factor those out into a helper method, perform the existing checks, and also ensure that a 2D variant of self.single_point equals a 2D variant of the expected result:

# Tests single_point against [-3, -2]
# Tests multiple_points against [[-6, -0], [-9, -6], [0, -8]]
# Tests the 2D variant of single_point to ([-3, -2])
_run_transform_on_points(trans, [-3, -2],
    [[-6, -0], [-9, -6], [0, -8]])
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

timhoffm
timhoffm reviewed Nov 1, 2026
View reviewed changes
Comment thread lib/matplotlib/tests/test_transforms.py Outdated
assert_array_almost_equal(r90.transform(self.single_point), [-1, 1])
assert_array_almost_equal(r90.transform(self.multiple_points),
[[-2, 0], [-3, 3], [0, 4]])
self._check_single_point(r90, [-1, 1])

@timhoffm timhoffm Nov 1, 2026

Copy link
Copy Markdown
Member

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’m not clear this is actually an improvement. It creates more indirection and makes the test harder to follow.
In fact, I’d claim self.single_point is an anti pattern. I’d rather have the value spelled out

Suggested change
self._check_single_point(r90, [-1, 1])
assert_array_almost_equal(r90.transform([1, 1]), [-1, 1])

Read: “r90 transforms [1, 1] approximately to [-1, 1]”

If you want to abstract from here, it should be something like

Suggested change
self._check_single_point(r90, [-1, 1])
self._check_transform(r90, [1, 1], [-1, 1])

I also would not create separate check functions for multiple points.

Either explicitly spell this out as

Suggested change
self._check_single_point(r90, [-1, 1])
self._check_transform(r90, [1, 1], [-1, 1])
self._check_transform(r90, [[1, 1]], [[-1, 1]])

or absorb the logic into _check_transform, i.e. detect if it has a single point passed and then additionally test the point wrapped in a sequence.

For multiple points, stating the inputs is a bit longer and may need wrapping, but I think that’s still preferable to a magic distant input.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@iccir iccir Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

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'm not a fan of self.single_point or self.multiple_points either, I think they make it hard to figure out what the test is actually doing since you have to find the top of the class in your editor. I wasn't sure what the team's preference was, however.

I like _check_transform and absorbing the logic for single point detection. If we do future optimizations (such as optimizing bbox or 2 points), that would also give us the ability to run both pathways as needed.

Are you ok with the exact keyword argument with a default of False and having some tests opt into assert_array_equal? If not, it would probably be easier to simply call assert_array_equal/assert_array_almost_equal, remove single_point/multiple_points, and add the "single point as 2D" cases manually.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@timhoffm timhoffm Nov 1, 2026

Copy link
Copy Markdown
Member

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’m fundamentally ok with exact. I only don’t know what the criterion for (not) using exact is. It currently seems random. But I’m ok to accept the current choices as the status quo.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

iccir reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

@iccir iccir Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

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.

The existing use of assert_array_almost_equal is due to floating point imprecision.

For example, TestAffine2D.test_skew uses Affine2D().skew_deg(26.5650512, 14.0362435). Applying that to (1, 1) results in a point close to (1.5, 1.25), but not quite. If we were to set exact=True:

E       AssertionError: 
E       Arrays are not equal
E       
E       Mismatched elements: 2 / 2 (100%)
E       Mismatch at indices:
E        [0]: 1.5000000005000806 (ACTUAL), 1.5 (DESIRED)
E        [1]: 1.2500000005947753 (ACTUAL), 1.25 (DESIRED)
E       Max absolute difference among violations: 5.9477534e-10
E       Max relative difference among violations: 4.75820272e-10
E        ACTUAL: array([1.5 , 1.25])
E        DESIRED: array([1.5 , 1.25])

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@iccir iccir Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

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 would probably be better to change that specific test case to use math.degrees(math.atan(0.5)) rather than 26.5650512 and then use exact=True.

That works on my machine (arm64), but it may not be portable to other architectures, compilers, and/or versions of Python/numpy.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@iccir iccir Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

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.

Sounds good to me!

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@iccir iccir Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

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 think the rotations around 90 degrees can't be exact due to the matrix containing values near 0:

>>> trans = t.Affine2D().rotate_deg_around(1.0, 1.0, 90).rotate_deg(180)
>>> trans
<matplotlib.transforms.Affine2D object at 0x1039f4590>
>>> trans.get_matrix()
array([[-1.8369702e-16,  1.0000000e+00, -2.0000000e+00],
       [-1.0000000e+00, -1.8369702e-16,  2.4492936e-16],
       [ 0.0000000e+00,  0.0000000e+00,  1.0000000e+00]])

The scale(3, -2).translate(23, 42) case probably should be exact; however, as you said, we can address that in a different PR.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@iccir iccir Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

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.

@timhoffm - Since I touched a lot of the tests in this PR, would it be better to back those changes out and do a follow-up PR that only touches the tests?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@timhoffm timhoffm Nov 2, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

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 strong opinion. Whatever is less work. I’ve already approved. So there’s only one review left. If you can describe to / direct the second reviewer so that they can easily churn through the rest changes, that’d be good enough.

OTOH separating refactoring from optimization is generally good.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@iccir iccir Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

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 strong opinion. Whatever is less work. I’ve already approved. So there’s only one review left. If you can describe to / direct the second reviewer so that they can easily churn through the rest changes, that’d be good enough.

You are the second reviewer :) Scott already approved this, minus the test case changes. I'm going to back out the test case refactoring and put that in a separate PR.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@iccir
iccir force-pushed the affine-cleanup branch 2 times, most recently from 9de1f43 to 30cf0fd Compare November 2, 2026 15:45
scottshambaugh
scottshambaugh approved these changes Nov 2, 2026
View reviewed changes

@scottshambaugh scottshambaugh left a comment

Copy link
Copy Markdown
Contributor

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.

Still lgtm

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

iccir reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@iccir

iccir commented Nov 2, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

Still lgtm

If @timhoffm is ok with it, I think I'd rather have a follow-up PR that only touches the tests and merge this as-is.

timhoffm reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@iccir iccir mentioned this pull request Nov 2, 2026
Cleanup test_transforms #32567
Open
2 tasks done
@iccir iccir added this to nanobind Nov 2, 2026
@github-project-automation github-project-automation Bot moved this to Todo in nanobind Nov 2, 2026
@iccir iccir moved this from Todo to Ready for Review in nanobind Nov 2, 2026
@timhoffm
timhoffm merged commit 95a3d29 into matplotlib:main Nov 3, 2026
77 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@github-project-automation github-project-automation Bot moved this from Ready for Review to Done in nanobind Nov 3, 2026
@timhoffm timhoffm added this to the v3.12.0 milestone Nov 3, 2026
@iccir
iccir deleted the affine-cleanup branch November 3, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@timhoffm timhoffm timhoffm approved these changes
@scottshambaugh scottshambaugh scottshambaugh approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

Performance topic: path handling topic: transforms and scales

Projects

nanobind
Status: Done

Milestone

v3.12.0

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

3 participants

@iccir @timhoffm @scottshambaugh
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.