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

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