Skip to content
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

left/right inverses? #10

Open
cscherrer opened this issue Nov 12, 2021 · 20 comments
Open

left/right inverses? #10

cscherrer opened this issue Nov 12, 2021 · 20 comments

Comments

@cscherrer
Copy link
Contributor

Just adding a note here (we don't have to dig in right now), that it would be great to also support left_inverse and right_inverse, since this comes up so often

@oschulz
Copy link
Collaborator

oschulz commented Nov 12, 2021

Good point, let's keep this issue as a reminder.

@sethaxen
Copy link

I agree, this would be useful to have. left_inverse and right_inverse could fall back to inverse, so that if a downstream user only needs to require that a function be right-invertible, then they would call right_inverse directly and support more functions.

@oschulz
Copy link
Collaborator

oschulz commented Feb 10, 2022

Also relates to #8, e.g. for square/sqrt.

@oschulz
Copy link
Collaborator

oschulz commented Feb 12, 2022

right-inverses could also be helpful components (building blocks) in pullback definitions, see example in JuliaObjects/Accessors.jl#46 (right_inverse(@optic(_.a) would be it's pullback, modulo ChainRulesCore boilerplate).

@ParadaCarleton
Copy link

ParadaCarleton commented Nov 2, 2023

@oschulz I see sqrt and square are defined as inverses, even though this isn't strictly correct; is the plan to allow partial inverses (e.g. trig/hyptrig) functions to be defined as inverse, or should I define a new function partial_inverse for this case? (We've come across it in TableTransforms.jl, where we need left inverses.)

@oschulz
Copy link
Collaborator

oschulz commented Nov 2, 2023

@ParadaCarleton , so far we haven't really had a use case for left/right inverses, but I guess now there is one with TableTransforms then. I think there's consensus that in principle we should add left/right inverses. Would you like to put a proposal together, maybe as a draft PR - just bare-bone initially, so we can discuss the API?

@aplavin
Copy link
Contributor

aplavin commented Dec 25, 2023

Noticed interesting convergence, though arriving from quite a different direction.

In AccessorsExtra, I define the construct([T,] f1 => val1, f2 => val2, ...) function to create an object with the given values function. The result is an object of type T with f1(obj) == val1, f2(obj) == val2, ... .

In the general case, construct methods have to be defined for each set of functions that one wants to create an object from, such as

construct(Gaussian, area => 1, fwhm => 2)
construct(Complex, abs => 3, angle => 0.5)

However, invertible functions and basic types like (named)tuples are supported automatically. This means

julia> rinverse(f) = x -> construct(f => x)

is a right inverse for supported functions.

# just uses inverse() inside
julia> rinverse(log10)(3)
1000.0

# uses inverse() + creates namedtuple
julia> rinverse(@o log10(_.a))(3)
(a = 1000.0,)

I don't really have common usecases for right inverses to non-fully-invertible mathematical functions, so this rinverse only supports invertible functions + property or index accessors.

It can be seen from these examples that right_inverse is kinda "more general" than the full inverse and can naturally be applied to things like property accessors – not just mathematical functions.

@oschulz
Copy link
Collaborator

oschulz commented Dec 25, 2023

rinverse(f) = x -> construct(f => x)

Oh, this looks elegant @aplavin !

I tried running construct(log => 0.5) , but I get no method matching default_empty_obj(::typeof(log)) - how do I tell construct what to build?

@oschulz
Copy link
Collaborator

oschulz commented Jan 2, 2024

Works beautifully @aplavin ! :-)

@oschulz
Copy link
Collaborator

oschulz commented Jan 2, 2024

I've been thinking, if we offer left/right-inverses, should we allow room in the API and namespace for non-function inverses, and a more general package "InverseElements.jl" (or simply "Inverses.jl")?

Our current inverse(f) could mathematically (an inverse being in relation to a binary operation and a neutral element) be written as inverse_element(∘, identity, f). Such an API would then also allow for inverse_element(+, 0, 5) and so on. I don't think we need to pass the neutral element explicitly though, I think we can treat it as implied by the operation and the object.

So we could have a package InverseElements that defines inverse_element(op, object), it could depend on InverseFunctions and define inverse_element(::typeof(∘), f) = inverse(f).

But what about left_inverse and right_inverse? Should they be for functions only, then we'd also need left_inverse_element and right_inverse_element for the general case, or should we require passing op (so for functions) explicitly for left_inverse and right_inverse in general?

@oschulz
Copy link
Collaborator

oschulz commented Jan 2, 2024

Works beautifully not

Quite hard to parse as a non-native speaker – does it work or doesn't? :)

It does work - my bad, the perils of typing on the phone on a train. :-)

@oschulz
Copy link
Collaborator

oschulz commented Jan 2, 2024

Interesting thoughts about inverse_element, do you have any specific usecases in mind?

We would actually have at lease one nice internal use case: We could use expand inverse support for Base.Fix1 and Base.Fix2 and make it more elegant and less awkward dispatch-wise.

Let's say we do it all in "InverseFunctions.jl" (no extra package "Inverse[Elements].jl"). "InverseFunctions" can mean "functions the generate inverses" just as well as "inverses of functions", after all. Then we could have a generic implementation

inverse(ff::Base.Fix1) = Base.Fix1(ff.op, left_inverse(ff.op, ff.x))
inverse(ff::Base.Fix2) = Base.Fix2(ff.op, right_inverse(ff.op, ff.x))

And with left/right-inverses for specific binary operations

left_inverse(::typeof(-), x::Real) = x
right_inverse(::typeof(-), x::Real) = -x

we'd automatically get

julia> f = Base.Fix1(-, 5); inverse(f)(f(9))
9

julia> f = Base.Fix2(-, 5); inverse(f)(f(9))
9

We still might want to specialize inverse of Base.Fix1 and Base.Fix2 for certain operations (e.g. so that \ is used in case of inv to inverse matrix multiplication), but in general the above should work nicely, I think.

@oschulz oschulz mentioned this issue Jan 2, 2024
@oschulz
Copy link
Collaborator

oschulz commented Jan 2, 2024

*'s inverse would be / for performance/accuracy

Not necessarily, at least not for numbers - if the inverse is used more than once, then precomputing 1/x would result in much higher performance (since division is so much slower than multiplication), and I think accuracy wouldn't suffer (I think many ALUs compute division via multiplication with inverse anyway). For matrices it's a different story - precomputing the inverse might seem attractive if the inverse is used multiple times, but as far as I know using \ can be a lot more stable numerically (depending on the matrix) than multiplication with the inverse matrix.

Certainly wouldn't hurt to define left_inverse and right_inverse for all of +, -, * and /, and then only specialize Base.Fix... inverses where required, I'd say.

^'s inverse cannot be done with ^ at all

Unfortunately not - (b ^ e) ^ (1//x) does work even for negative b, but ^ is not associative.

So yes, we'd still need quite a few explicit Base.Fix... inverses, but I think it would still be elegant. And it would add left- and right-inverses in a way that extends the scope of the fashion in a natural and self-consistent fashion.

@MilesCranmer
Copy link

What’s the current right way to handle this? I’m looking to use something like this in SymbolicRegression. FWIW I like the inverse(::Base.Fix1/2) idea, which was the first thing I tried before hitting the error.

@oschulz
Copy link
Collaborator

oschulz commented Jul 13, 2024

What’s the current right way to handle this?

Left/right- inverses of a specific inverse case involving FixN? For left/right-inverses we need to add that to the API - I think there's consensus that we want to have it, but no one has done a PR with an API for it yet. :-)

@MilesCranmer
Copy link

Gotcha.

Just to check, is the following the right idea?

inverse(f::Base.Fix1{typeof(+)}) = Base.Fix2(-, f.x)

I'm starting out with an internal method that forwards to InverseFunctions to avoid piracy and since for my use-case it's useful to have non-exact inverses (like cos -> acos or even things like abs -> abs) here: https://github.com/MilesCranmer/SymbolicRegression.jl/blob/a7f207abd9518c6e9f9a59bc1d64a8c2e1de72da/src/InverseFunctions.jl

@MilesCranmer
Copy link

MilesCranmer commented Jul 14, 2024

I guess there's some ambiguity here... not sure if there's a better way

inverse(f::Base.Fix1{typeof(+)}) = Base.Fix1(+, -f.x)

Some are quite nice though:

inverse(f::Base.Fix1{typeof(-)}) = f
inverse(f::Base.Fix1{typeof(/)}) = f

@MilesCranmer
Copy link

MilesCranmer commented Jul 14, 2024

For posterity, here's what I put into SR.jl. Note that I label these as approx_inverse just to avoid pirating InverseFunctions.jl. And also for the fact that some may not have inverses for part of the domain (still useful for my stuff)

# (f.x + _) => (_ - f.x)
approx_inverse(f::Base.Fix1{typeof(+)}) = Base.Fix2(-, f.x)
# (_ + f.x) => (_ - f.x)
approx_inverse(f::Base.Fix2{typeof(+)}) = Base.Fix2(-, f.x)

# (f.x * _) => (_ / f.x)
approx_inverse(f::Base.Fix1{typeof(*)}) = Base.Fix2(/, f.x)
# (_ * f.x) => (_ / f.x)
approx_inverse(f::Base.Fix2{typeof(*)}) = Base.Fix2(/, f.x)

# (f.x - _) => (f.x - _)
approx_inverse(f::Base.Fix1{typeof(-)}) = f
# (_ - f.x) => (_ + f.x)
approx_inverse(f::Base.Fix2{typeof(-)}) = Base.Fix2(+, f.x)

# (f.x / _) => (f.x / _)
approx_inverse(f::Base.Fix1{typeof(/)}) = f
# (_ / f.x) => (_ * f.x)
approx_inverse(f::Base.Fix2{typeof(/)}) = Base.Fix2(*, f.x)

# (f.x ^ _) => log(f.x, _)
approx_inverse(f::Base.Fix1{typeof(^)}) = Base.Fix1(log, f.x)
# (_ ^ f.x) => _ ^ (1/f.x)
approx_inverse(f::Base.Fix2{typeof(^)}) = Base.Fix2(^, inv(f.x))

@MilesCranmer
Copy link

D’oh!! I need sleep…

@oschulz
Copy link
Collaborator

oschulz commented Jul 14, 2024

@MilesCranmer , if you don't specifically need FixN, you can also use AffineMaps, it has dedicated objects for Mul, Add, MulAdd and AddMul, with support for InverseFunctions and ChangesOfVariables. So it would be Add(b) instead of Base.Fix2(+, b).

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

No branches or pull requests

6 participants