Skip to content

Replace Transform.__add__ with Transform.__matmul__? #17230

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

Closed
brunobeltran opened this issue Apr 23, 2020 · 6 comments
Closed

Replace Transform.__add__ with Transform.__matmul__? #17230

brunobeltran opened this issue Apr 23, 2020 · 6 comments

Comments

@brunobeltran
Copy link
Contributor

brunobeltran commented Apr 23, 2020

Bug report

Bug summary

While working on fixing the transform pipeline for image rendering, it struck me that the operator "+" probably sends the wrong impression to users in terms of expected properties. The Euclidean group is barely associative, and definitely not commutative.

Also, since a Transform is something that's applied (like an operator), my expectation as I'm using composite transforms is that they will be applied rightmost-first. In other words, I keep expecting

>>> (A + B).transform(X) == A.transform(B.transform(X))  # but it's currently False
>>> (A + B).transform(X) == B.transform(A.transform(X))  # how it currently works.

Now that Python has the "@" operator, this feels like a more natural choice for "combining" Transforms. People don't generally expect A@B == B@A, while they do expect A + B == B + A, which is definitely not true for Transforms.

The implementation would just be something like (modulo deprecation code)

def __matmul__(self, other):
    return other + self

And we would get

>>> (A @ B).transform(X) == A.transform(B.transform(X))  # returns True

Release strategy

Fixing all our internal uses is (probably) only one PR's worth of work, since (relative to how much code would churn) it would be easy to verify/review.

However, to me this feels like an integral enough part of the Transform API that it might make sense to leave in "+" until 4.x, and maybe just make sure we only advertise "@" in e.g. the tutorials?

In summary:

Pros

  1. "@" implies lack of commutivity, whereas "+" implied commutivity
  2. "@" implies order of application (rightmost-first), whereas "+" is ambigious

Cons

  1. Pretty core part of Transform API
  2. Lots of code-churn in Transform-related internals.
@brunobeltran
Copy link
Contributor Author

brunobeltran commented Apr 23, 2020

Oh, forgot the main reason why I started being bothered by this.

A lot of places in the code use "+=" to combine transforms, such as in _make_image, where we do

            t += Affine2D().scale(1.0 + extra_width, 1.0 + extra_height)

In this case (and often, in general), whether this gets added on the right or left matters. So having it be feel ambiguous with "+=" makes life harder for someone trying to read/understand this code.

At least, I know I personally have to drop a break point (or lookup how __add__ interacts with "+=" in the Python docs) anytime I see this kind of thing.

@anntzer
Copy link
Contributor

anntzer commented Apr 23, 2020

This was just discussed in #17206 (comment) / #17217 / #17218. I personally think +/- actually work better here (well, in particular there's no - for @ (aka no left-divide) and that's a real problem).

@brunobeltran
Copy link
Contributor Author

brunobeltran commented Apr 23, 2020

Oh sorry, github didn't recommed those to me, I'll close this.

@brunobeltran
Copy link
Contributor Author

brunobeltran commented Apr 23, 2020

Seems like the main con is actually losing the ability to cleverly make

A @ A.inverted() == IdentityTransform()

return True.

So in order to get feature parity, we'd need .inverted to return an InvertedTransform of some sort so that CompositeGenericTransform can check if things cancels exactly when evaluating "@".

Seems like an additional complaint is also that B**-1 @ A and (1/B) @ A aren't as "pretty" as "A - B". I disagree, since we don't typically have long chains of transforms to make it unwieldy to just type B.inverted() @ A. And if we do, I think that's honestly more readable than B - A. The latter is now well documented (thanks @anntzer!) but to be fair, the former doesn't even need documentation. It just works the way you think it does.

To me, it seems like having code that makes A.inverted() @ A evaluate to exactly IdentityTransform is no better or worse than having special code to check that A - A does (i.e. hiding that behavior in a separate operator seems unnecessary except for ease of implementation ).

I'll reopen this if people are favorable to something like this (i.e. .inverted to return an InvertedTransform of some sort so that CompositeGenericTransform can check if things cancels exactly when evaluating "@") happening, but seems like on top of this now being tricky to implement there's considerable push back on changing it at all.

@anntzer
Copy link
Contributor

anntzer commented Apr 24, 2020

Having a real InvertedTransform() was proposed at least once (by @ImportanceOfBeingErnest iirc? you'll have to look it up) to get a non-frozen inverse() (i.e. one that keeps track of changes in the original thing); I think it would be usefule, and I guess it would be a prerequisite for a switch to matmul to happen (even though right now I still don't like it).

Copy link
Contributor Author

Looks like it was proposed in #10741.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants