On Wed, Mar 31, 2021 at 12:24 AM JSS95 <[email protected]> wrote: > > In SymPy, we have two ways to get the mathematical property of an object : > the old assumptions system and the new assumptions system. > With old assumptions system, the property is accessed by `.is_[...]` > attribute. Here, property of the object is intrinsic. > ``` > In [1]: x = Symbol('x', positive=True) > In [2]: x.is_positive > Out[2]: True > ``` > With new assumption system, the property is accessed by assumption predicate > and `ask()` function. Here, property of the object can be extrinsically > assumed. > ``` > In [1]: x = Symbol('x') > In [2]: ask(Q.integer(x), Q.even(x)) > Out[2]: True > ``` > > Old assumption system is used in evaluation logic because it is faster. In > refinement logic where user can pass local assumptions, new assumption system > is used. > This results the duplication in two methods. For example, evaluation logic of > `Abs` is vaguely like this: > ``` > def eval_Abs(x): > if x.is_nonnegative: > return x > ... > ``` > And refinement logic of `Abs` is like this: > ``` > def eval_Abs(x, assumptions=True): > if ask(Q.nonnegative(x), assumptions): > return x > ... > ``` > > As you can see, two codes are mostly identical and the only difference is old > assumptions vs new assumptions. By introducing a function which can switch > between two behaviors, we can remove this duplication. > > The first suggestion is to pass a getter function. > ``` > def eval_Abs(x, getter): > if getter(x, 'nonnegative'): > return x > ... > ``` > Here, we can pass `lambda x, key : getattr(x, "is_%s" % key)` to getter for > evaluation, and `lambda x, key : ask(getattr(Q, key)(x), assumptions)` for > refinement. > Pros : > - Easy to implement > Cons: > - `getattr(x, "is_%s" % "positive")` is slower than `x.is_positive`. > - `getter(x, '...')` everywhere looks ugly. > > The second suggestion is proposed by oscarbenjamin in > https://github.com/sympy/sympy/pull/21203#issuecomment-810134012. > If I understood correctly, the idea is to introduce `AssumptionsWrapper` > class like this: > ``` > class AssumptionsWrapper(Basic): > def __new__(cls, expr, assumptions=True): > if assumptions == True: > return expr > obj = super().__new__(cls, expr, assumptions) > obj.expr = expr > obj.assumptions = assumptions > return obj > def _eval_is_positive(self): > return ask(Q.positive(self.expr), self.assumptions) > ``` > And `eval_Abs` will be like this: > ``` > def eval_Abs(x, assumptions=True): > _x = AssumptionsWrapper(x, assumptions) > if _x.is_positive: > return x > ... > ``` > Pros : > - No slowdown for retrieving `.is_positive` attribute with old assumptions > Cons: > - Generating wrapper objects is slow. > - Having wrapped objects everywhere makes the code messy > > And here is the third suggestion, which is to modify `ManagedProperties` > (metaclass for `Basic`) to automatically define `check_[...]` methods for > every keys. > The design will be like this: > ``` > ... > # This method will be automatically generated by metaclass > def check_positive(self, assumptions=True): > if assumptions is True: > return self.is_positive > else: > return ask(Q.positive(self), assumptions) > ``` > And `eval_Abs` will be: > ``` > def eval_Abs(x, assumptions=True): > if x.check_nonnegative(assumptions): > return x > ... > ``` > Pros: > - Easy to implement. I tried this, and it required only <20 more lines in > core/assumptions file > - Fastest > - Cleaner code > Cons: > - Basic will be cluttered up with more methods
Indeed. It seems you are proposing to fix the problem of there being two different ways to do things by adding a third way https://xkcd.com/927/. I'm -1 to adding another syntax for checking assumptions. I've always seen the preferred solution to this problem being to merge the two assumptions systems, so that x.is_positive and ask(Q.positive(x)) do the same thing. The former would just be a convenient shorthand for the cases that it can represent. If you need to pass in assumptions in a way that isn't supported by the "old assumptions", you can use ask() instead. This has already partly been done in that ask() checks the assumptions set of Symbol. The reverse is harder because ask() is currently too slow. I think we can improve the performance of it. One thing we need to do is devise an evaluation scheme where queries can be faster but less accurate. That is, a fast query might return None when a slower query might return True or False. These fast queries would be preferred for places where performance matters. Probably a more relevant design question is the problem that the old assumptions system stores assumptions on objects, whereas ask() stores them as separate predicates. So the question of how is_... calls ask() isn't so obvious. Previously the idea for this was global assumptions contexts (with the assuming() context manager). I think this is a good idea, but passing assumptions through to functions as with Oscar's idea may also be more convenient, although it does represent a pretty significant API shift in SymPy. Another problem, which also relates to performance, is that we need to significantly reduce the use of assumptions in constructors. Automatic evaluation should only happen when it can be fast, and checking assumptions is antithetical to that. To me, your _eval_Abs above should actually be Abs.doit() or Abs._eval_refine(). Aaron Meurer > > -- > You received this message because you are subscribed to the Google Groups > "sympy" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to [email protected]. > To view this discussion on the web visit > https://groups.google.com/d/msgid/sympy/63e8baab-e299-4c15-a4c5-bc862fe406e3n%40googlegroups.com. -- You received this message because you are subscribed to the Google Groups "sympy" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion on the web visit https://groups.google.com/d/msgid/sympy/CAKgW%3D6%2BeN0_oASAyLaevq_YgFr1W_CO-_h8FX9FuiXNs5At%3DtA%40mail.gmail.com.
