sympy / sympy

A computer algebra system written in pure Python
https://sympy.org/
Other
12.8k stars 4.39k forks source link

Add inverse_set() method to generalize inverse() #23611

Open asmeurer opened 2 years ago

asmeurer commented 2 years ago

Right now, classes can define an inverse method to return an inverse function. This tells solve and solveset how to invert the function. It works like

class asin(Function):
    def inverse(self, argindex=1):
        return sin

The problem is that because it returns a single function object, it only really works for one-to-one functions (left inverse). Functions that are multivalued can't return a single inverse function. Some multivalued functions have inverse defined anyway (like exp) and some do not (like sin).

I propose generalizing this with a new method inverse_set. It would look like

class sin(Function):
    def inverse_set(self, symbol, argindex=1):
        n = Dummy('n', integer=True)
        return ImageSet(Lambda(n, (-1)**n*asin(symbol) + n*pi), Integers)

(see https://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Solutions_to_elementary_trigonometric_equations).

solveset would then use this to solve expressions containing sin (for known functions like sin, it may also use other algorithms and rewrites in addition to this). For instance, solveset(sin(x) - y, x) would be equivalent to sin(x).inverse(y) (n.b. solveset currently can't handle this equation). We could also define a general inverse_set on the Function base class that returns a ConditionSet, although it might only be convenient for end-users of the method.

For solve to make use of inverse_set, we would need to implement more general translation from solveset to solve, which needs to be done anyway.

Note that inverse_set would primarily only be defined on Function subclasses. The logic in solveset would still be needed to invert the functions inside of larger expressions.

It may make more sense for it to be method that starts with an underscore, like _eval_inverse_set, so that users aren't encouraged to use it directly. Although this is a much more end-user friendly API since it would return an actual expression rather than a callable function.

oscarbenjamin commented 2 years ago

In some situations it's more useful to be able to say "give me any local inverse" rather than a full inverse.

Where a full inverse is needed it might be more useful to have a preimage function i.e. something that answers the question: what is the set of values for x such that f(x) = y (with y being a single value rather than a set)?

asmeurer commented 2 years ago

In some situations it's more useful to be able to say "give me any local inverse" rather than a full inverse.

If you just want one such example you can use next(iter(expr.inverse_set())), e.g.,

>>> next(iter(ImageSet(Lambda(n, asin(y) + n*pi), Integers)))
asin(y)

(or the take helper).

Where a full inverse is needed it might be more useful to have a preimage function i.e. something that answers the question: what is the set of values for x such that f(x) = y (with y being a single value rather than a set)?

Maybe I'm misunderstanding what you're suggesting here, but wouldn't this be the same as my suggested inverse_set? I'm OK with using preimage instead as a more mathematically correct name. I suggested inverse_set in analogy with the already existing inverse.

The main point here is there needs to be a way for functions to fully define their behavior for solve/solveset. Currently only one-to-one functions can.

oscarbenjamin commented 2 years ago

If you just want one such example you can use next(iter(expr.inverse_set())), e.g.,

I expect that there are many situations where that might fail or might do the wrong thing. It's much better to have a clear way to say "just give me any inverse and don't waste time computing anything more difficult or complicated than that".

Maybe I'm misunderstanding what you're suggesting here, but wouldn't this be the same as my suggested inverse_set?

Actually yes but maybe rename the "symbol" argument. The idea is that it is really a value so that we are computing the preimage for a particular element rather than returning an inverse "function".

asmeurer commented 2 years ago

I expect that there are many situations where that might fail or might do the wrong thing. It's much better to have a clear way to say "just give me any inverse and don't waste time computing anything more difficult or complicated than that".

I'd say this falls into the more general category of helpers to convert sets and results from solveset into useful expressions, including into the sort of answer that solve might return. The set itself is the most general answer and should, in principle, contain enough information to compute this. That might depend on representing the set in a convenient way, but that's not a big problem (e.g., if an ImageSet is easier to iterate over than a ConditionSet then we should try to represent the solution set as an ImageSet).

So really, I think the focus should be on improving the ability to compute things on sets and simplify set expressions. If we do that, then we can work with the general solutions while still being able to extract useful expressions from them.

asmeurer commented 2 years ago

Another thing I forgot to mention is that much like fdiff, the result should be independent of the actual args of the function. Recursive solving is a problem for solve. Maybe it would make sense to make this a classmethod to emphasize that?

oscarbenjamin commented 2 years ago

I'd say this falls into the more general category of helpers to convert sets and results from solveset into useful expressions

No, I don't think it does because I don't even want solveset et al to bother computing the things that they compute just so that I can extract a tiny subset of that afterwards.