How to Use InverseFunctions.jl that can Calculate the Inverse of a Function?

Hi all,

I read the package here:
https://juliamath.github.io/InverseFunctions.jl/stable/

I want to calculate simple inverse of a function: y^{2} = 4x

I use this:

using InverseFunctions

foo(x) = sqrt(4x)

inverse(foo)

NoInverse{typeof(foo)}(foo)

Why it does not return the inverse of a function which is \frac{y^{2}}{4}
Another question, I want to know another alternative to get the inverse of a function besides that package.

This package wants you to express the function as a composition of functions:

julia> using InverseFunctions

julia> foo = sqrt ∘ Base.Fix1(*, 4)
sqrt ∘ Base.Fix1{typeof(*), Int64}(*, 4)

julia> inverse(foo)
Base.Fix1{typeof(\), Int64}(\, 4) ∘ InverseFunctions.square

julia> inverse(foo).(1:8)
8-element Vector{Float64}:
  0.25
  1.0
  2.25
  4.0
  6.25
  9.0
 12.25
 16.0

It does raise interesting questions about function composition fallbacks, something that’s recently been on my mind (see here). It could be interesting if Julia allowed our own fallbacks for this kind of purpose. Calculating inverse functions sure is handy, e.g., when using the Kolmogorov-Nagumo Average.

Edit: On further thought, we wouldn’t need any custom fallback to satisfy this desire beyond what was suggested in that link (i.e., a composition fallback for partially-applied functions).

1 Like

Building on concepts from PR#24990 and function composition


julia> using ChainingDemo # my demo code for underscore partial application

julia> InverseFunctions.inverse(f::ComposedPartialFunction) = inverse(f.f)

julia> InverseFunctions.inverse(f::Fix{F,()}) where F = inverse(f.f)

julia> InverseFunctions.inverse(f::Fix1_2) = inverse(Base.Fix1(f.f, f.fixvals[1]))

julia> f(x) = √(4x)
f (generic function with 1 method)

julia> @underscores@show g=inverse(f(identity(_)));
g = inverse(f(Fix{(), 1}(identity))) = identity ∘ Base.Fix1{typeof(\), Int64}(\, 4) ∘ InverseFunctions.square

julia> g(8)
16.0

RaccoonYesGIF

2 Likes

Can I have the function g in terms of y ?

ChainingDemo is not available to be downloaded now.

Yes, g is a function of y.

ChainingDemo is just some demo code I made to demonstrate my ideas for a language proposal (available in the link above), demonstrating an implementation of PR#24990 to make _ underscores create partial functions. In that demo code I overloaded a “whitelist” of basic functions so that calling them on partial functions would invoke function composition.

Unfortunately, these ideas aren’t very robust when not implemented as a proper language feature (a function composition fallback is preferable to overloading, due to type inference dispatch ambiguities that arise from overloading).

So until PR#24990 can be approved, we will just have to live with typing out foo = sqrt ∘ Base.Fix1(*, 4) instead.

Accessors.jl helps defining a function as a composition:

julia> using Accessors, InverseFunctions

julia> foo = @optic sqrt(4*_)
sqrt ∘ Base.Fix1{typeof(*), Int64}(*, 4)

julia> inverse(foo)
Base.Fix1{typeof(\), Int64}(\, 4) ∘ InverseFunctions.square

Accessors are composable and have much more superpowers, but the above is also a common nice usecase. Accessors.jl is a very stable, lightweight and widely used package.

3 Likes

Ah yes, this is a nice option too.

I still don’t get this:

why defining a function as a composition to get the inverse of a function?

Function composition is just a convenient way to represent a sequence of function calls, because it stores the functions in a data structure and it’s supported by the language.

Consider the function h(x)=\sqrt{4x}. We can represent it as h(x)=f(g(x)) where f(x)=\sqrt x and g(x)=4x.

Mathematics provides a function composition operator \circ, which allows you to write h as a composition of f and g, namely, h=f\circ g. The rules are, h(x) = f(g(x)) \Leftrightarrow h(x)= (f\circ g)(x). The neat thing about this is that h can be represented strictly in terms of other functions, and not in terms of x, and so if you know the properties of those functions you can reason about the properties of h.

Julia, being a language heavily inspired by mathematics, provides a function composition operator ∘ (\circ<tab>). The rules work the same as in mathematics, and when I write h=f∘g, the result is an object that can be called like any other function, h(x) == (f∘g)(x), but the object h has information about what functions it’s made of. If you’re trying to take the inverse of h, this is super useful because you can simply interrogate the h object to find what functions it’s made of, take the inverse of those (which you know from a lookup table), and compose a new function with those inverses in reversed order.

To see how it works in detail, go to line 1024 here. Basically, h::ComposedFunction has h.inner and h.outer fields that contain the functions it’s made of.

1 Like

Assuming f,g are invertible, it holds (f \circ g)^{-1} = g^{-1} \circ f^{-1} with f^{-1} the inverse of f.

4 Likes

There are different possible ways to automatically calculate function inverses:

  • Analytical, as implemented by InverseFunctions. Basically, this package is a database of known invertible functions with their inverses, and a mechanism of computing inverses for function compositions: inverse(f ∘ g) = inverse(g) ∘ inverse(f).
  • Numerical: computing f_inv(x) = y involves solving f(y) = x. This is not how InverseFunctions work.

So, the analytical approach works when the function is in the “database” itself, or is a composition of known functions. When you define f(x) = sqrt(4x) and try inverse(f), the f function is opaque to the inverse function: it doesn’t know what operations are inside. So, f should be an explicit Julia function composition.

2 Likes

I have been trying to find the inverse of f through the composition method, thanks to your explanation I can understand a bit more.

But I think I still can’t find the inverse of h.

Capture d’écran_2022-12-23_12-16-02

I define the function f(x) and g(x)

then type

inverse(f∘g)

gets:
NoInverse{ComposedFunction{typeof(f), typeof(g)}}(f ∘ g)

Your explanation makes me understand why need to use composition, my problem now is how to use InverseFunctions.jl correctly


This is what you must understand about using this package:

The only way that it “knows” how to invert a function, is through a lookup table.

For example, it has been programmed that the inverse of the function x\mapsto\sqrt x is y\mapsto y^2. But how does it know that a function is x\mapsto \sqrt x to begin with? By reference. The only way it “knows” that the inverse of sqrt is a squaring function, is because it has been programmed that the inverse of the function we reference with the identifier sqrt is a squaring function. And for the most part, this is how humans invert functions too.

This is best explained with an example:

julia> inverse(sqrt) # magic!
square (generic function with 2 methods)

julia> f(x) = sqrt(x) # same thing, right?
f (generic function with 1 method)

julia> inverse(f) # oh no! it has no idea!
NoInverse{typeof(f)}(f)

The moment we wrap sqrt in a new function, the package doesn’t know what the inverse is anymore. By inspection I can say that f(x)=\sqrt x for all x, but I can say this because I wrote it and I can see it; the inverse function has no clue what’s inside.

The same thing would happen if I blindfold you and give you a function f without telling what its definition is, and ask you to tell me the inverse; you wouldn’t be able to invert the function, even if it was really simple, because you don’t know what’s inside.

For contrast, let’s see what happens if we just create a new reference to the same function:

julia> w = sqrt # notice: this is just an assignment
sqrt (generic function with 19 methods)

julia> inverse(w) # it works!
square (generic function with 2 methods)

When I set w = sqrt, w is a new reference pointing to the same location in memory as sqrt, so inverse is able to identify that it’s the same thing.

As we’ve previously mentioned, when we create a more complicated function, we run into the same problem: inverse can’t tell what’s inside, unless we define the function as a composition of other functions.

This works because when I compose two functions using the ∘ operator, I create an object which has references to both functions. For example:

julia> exp ∘ sqrt
exp ∘ sqrt

julia> (exp ∘ sqrt).inner
sqrt (generic function with 19 methods)

julia> (exp ∘ sqrt).outer
exp (generic function with 14 methods)

This gives InverseFunctions.jl visibility into what functions have been composed to make the greater function, and as long as it has inverses defined for the inner and outer functions, it can invert the composition:

julia> inverse((exp ∘ sqrt).inner)
square (generic function with 2 methods)

julia> inverse((exp ∘ sqrt).outer)
log (generic function with 26 methods)

julia> inverse(exp ∘ sqrt)
InverseFunctions.square ∘ log

Notice that this works:

julia> s = exp
exp (generic function with 14 methods)

julia> t = sqrt
sqrt (generic function with 19 methods)

julia> inverse(s ∘ t)
InverseFunctions.square ∘ log

but this does not:

julia> u(x) = exp(x)
u (generic function with 1 method)

julia> v(x) = sqrt(x)
v (generic function with 1 method)

julia> inverse(u ∘ v)
NoInverse{ComposedFunction{typeof(u), typeof(v)}}(u ∘ v)

we can force it to work though by manually adding entries to the lookup table (by overloading the inverse function):

julia> InverseFunctions.inverse(::typeof(u)) = log

julia> InverseFunctions.inverse(::typeof(v)) = InverseFunctions.square

julia> inverse(u ∘ v)
InverseFunctions.square ∘ log

this isn’t useful in this case, because u and v are so simple they could have just been made to be references, but it could become useful later if you define more complicated functions.


So in your case, we want to invert a function x\mapsto \sqrt{4x}. We can write it as a composition of functions, (x\mapsto\sqrt x)\circ(x\mapsto 4x), but if we want to leverage InverseFunctions.jl’s power we need identifiers for these functions that it recognizes.

For the first function, we have the sqrt identifier.

And for the second? The function being called is multiplication, which is a binary operator. In order for multiplication to be invertible, we need to declare a single-argument function in terms of it: we need to fix one of its arguments. And that’s where Base.Fix1(*, 4) comes in: it’s a way to partially-apply the multiplication operator—resulting in a single-argument function—and because it’s built-in to the language, InverseFunctions.jl has been programmed to recognize it. (namely, for any two-argument function f, we can represent the function x\mapsto f(c, x) with Base.Fix1(f, c) and x\mapsto f(x, c) we can represent with Base.Fix2(f, c)).

And so we write:

julia> inverse(sqrt ∘ Base.Fix1(*, 4))
Base.Fix1{typeof(\), Int64}(\, 4) ∘ InverseFunctions.square

Now, something you have to be careful with, is that if you have previously declared h as a function (using named function syntax), you will get this error:

julia> h = inverse(sqrt ∘ Base.Fix1(*, 4))
ERROR: invalid redefinition of constant h

so we will need to either a) restart Julia so that we can give h a new meaning, or b) pick a different identifier such as inv:

julia> inv = inverse(sqrt ∘ Base.Fix1(*, 4))
Base.Fix1{typeof(\), Int64}(\, 4) ∘ InverseFunctions.square

julia> [inv(y) for y = 1:8]
8-element Vector{Float64}:
  0.25
  1.0
  2.25
  4.0
  6.25
  9.0
 12.25
 16.0

Now, if you think that learning about internals like Base.Fix1 for this purpose is unsavory, I agree. Getting PR#24990 approved, along with a generalized partial function applicator and a function composition fallback as laid out in points 1, 2, and 3 at the bottom of this post would solve this. But for now, if you just want something that works today and you don’t mind it being constrained to a macro call, @optic works quite well:

julia> using Accessors

julia> inv = inverse(@optic sqrt(4_))
Base.Fix1{typeof(\), Int64}(\, 4) ∘ InverseFunctions.square

It’s also possible to use a numerical solver, for example:

julia> using Roots

julia> f(x) = √(4x)
f (generic function with 1 method)

julia> [fzero(x->f(x)-y, 0) for y=1:8]
8-element Vector{Float64}:
  0.25
  1.0000000000000002
  2.25
  4.0
  6.25
  9.0
 12.25
 16.0

Before jumping to the conclusion that numerical solvers are magic and solve all our problems, you need to be aware that some people spend their entire lives making them work and they still don’t solve all our problems. They work sometimes though, and it’s nice when they do.

4 Likes

Thanks for all this explanations, I am still reading and try to comprehend all of them

1 Like