Skip to content

Conversation

kc611
Copy link
Contributor

@kc611 kc611 commented Dec 20, 2020

References to other Issues or PRs

Related to #20359

Brief description of what is fixed or changed

Added changes suggested by @oscarbenjamin in the issue page.

Release Notes

  • core
    • ordered() made faster for large expressions

@sympy-bot
Copy link

sympy-bot commented Dec 20, 2020

Hi, I am the SymPy bot (v161). I'm here to help you write a release notes entry. Please read the guide on how to write release notes.

Your release notes are in good order.

Here is what the release notes will look like:

  • core
    • ordered() made faster for large expressions (#20635 by @kc611)

This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.9.

Click here to see the pull request description that was parsed.
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234" (see
https://tinyurl.com/auto-closing for more information). Also, please
write a comment on that issue linking back to this pull request once it is
open. -->

Related to #20359 

#### Brief description of what is fixed or changed

Added changes suggested by @oscarbenjamin in the issue page. 

#### Release Notes

<!-- Write the release notes for this release below. See
https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more information
on how to write release notes. The bot will check your release notes
automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->

- core
  - ordered() made faster for large expressions

<!-- END RELEASE NOTES -->

Update

The release notes on the wiki have been updated.

@kc611
Copy link
Contributor Author

kc611 commented Dec 20, 2020

It seems there are multiple core test failures in modules other than core. Closing for now.

@kc611 kc611 closed this Dec 20, 2020
@oscarbenjamin
Copy link
Collaborator

It looks like most of the failures are for the same thing. There is some use of ordered with something that isn't Basic somewhere. Maybe that should be fixed. The docstring for ordered isn't very explicit about this but it is really supposed to be used for Basic.

@kc611 kc611 reopened this Dec 21, 2020
@czgdp1807 czgdp1807 added the core label Dec 21, 2020
@kc611
Copy link
Contributor Author

kc611 commented Dec 21, 2020

A rather crude workaround in the newly added _node_count did the trick. One thing I'm concerned is we may have overlooked a datatype's return value ( it seems that e.count(Basic) is expected to return a 0 instead of 1 when an int object is passed to it, there may be other such similar cases ).

Second issue here is the rather strange behavior of LatticeOP. It seems that when we pass *_args here.

obj = super(AssocOp, cls).__new__(cls, _args)

It fails some tests in sympy.logic.boolalg. Interesting thing to note is that when actually printing this failing test in console it gives exactly the same output as expected. But still assert fails. It seems that LatticeOP internally changes something within the original list of arguments which causes assert to fail.

@oscarbenjamin
Copy link
Collaborator

Interesting thing to note is that when actually printing this failing test in console it gives exactly the same output as expected. But still assert fails.

There is a deep bug here somehow. I tried the diff:

diff --git a/sympy/core/operations.py b/sympy/core/operations.py
index a6c0b92f2b..fa0d3e973d 100644
--- a/sympy/core/operations.py
+++ b/sympy/core/operations.py
@@ -496,7 +496,7 @@ def __new__(cls, *args, **options):
         else:
             # XXX in almost every other case for __new__, *_args is
             # passed along, but the expectation here is for _args
-            obj = super(AssocOp, cls).__new__(cls, _args)
+            obj = super(AssocOp, cls).__new__(cls, *_args)
             obj._argset = _args
             return obj

With that running

$ pytest sympy/logic/tests/test_boolalg.py

gives different random failures each time I run it.

Random failures like this could be due to set ordering. I can reproduce the problem semi-reliably by running this test in particular:

$ pytest sympy/logic/tests/test_boolalg.py -k test_to_nnf
====================================================== test session starts =======================================================
platform darwin -- Python 3.8.5, pytest-6.2.0, py-1.10.0, pluggy-0.13.1
architecture: 64-bit
cache:        no
ground types: python 

rootdir: /Users/enojb/current/sympy/sympy, configfile: pytest.ini
plugins: xdist-2.1.0, cov-2.10.1, doctestplus-0.8.0, forked-1.3.0
collected 71 items / 70 deselected / 1 selected                                                                                  

sympy/logic/tests/test_boolalg.py F                                                                                        [100%]

============================================================ FAILURES ============================================================
__________________________________________________________ test_to_nnf ___________________________________________________________

    def test_to_nnf():
        assert to_nnf(true) is true
        assert to_nnf(false) is false
        assert to_nnf(A) == A
        assert to_nnf(A | ~A | B) is true
        assert to_nnf(A & ~A & B) is false
        assert to_nnf(A >> B) == ~A | B
        assert to_nnf(Equivalent(A, B, C)) == (~A | B) & (~B | C) & (~C | A)
>       assert to_nnf(A ^ B ^ C) == \
            (A | B | C) & (~A | ~B | C) & (A | ~B | ~C) & (~A | B | ~C)
E       assert (A | B | C) & (A | ~B | ~C) & (B | ~A | ~C) & (C | ~A | ~B) == (((((A | B) | C) & ((~A | ~B) | C)) & ((A | ~B) | ~C)) & ((~A | B) | ~C))
E        +  where (A | B | C) & (A | ~B | ~C) & (B | ~A | ~C) & (C | ~A | ~B) = to_nnf(((A ^ B) ^ C))

sympy/logic/tests/test_boolalg.py:506: AssertionError
======================================================== warnings summary ========================================================
../38venv/lib/python3.8/site-packages/_pytest/config/__init__.py:1233
  /Users/enojb/current/sympy/38venv/lib/python3.8/site-packages/_pytest/config/__init__.py:1233: PytestConfigWarning: Unknown config option: doctestplus
  
    self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")

-- Docs: https://docs.pytest.org/en/stable/warnings.html
                                                         DO *NOT* COMMIT!                                                         
==================================================== short test summary info =====================================================
FAILED sympy/logic/tests/test_boolalg.py::test_to_nnf - assert (A | B | C) & (A | ~B | ~C) & (B | ~A | ~C) & (C | ~A | ~B) == (...
========================================== 1 failed, 70 deselected, 1 warning in 0.34s ===========================================

@oscarbenjamin
Copy link
Collaborator

Looks like a few more changes are needed to fix it:

diff --git a/sympy/core/operations.py b/sympy/core/operations.py
index a6c0b92f2b..7164faa03d 100644
--- a/sympy/core/operations.py
+++ b/sympy/core/operations.py
@@ -494,9 +494,7 @@ def __new__(cls, *args, **options):
         elif len(_args) == 1:
             return set(_args).pop()
         else:
-            # XXX in almost every other case for __new__, *_args is
-            # passed along, but the expectation here is for _args
-            obj = super(AssocOp, cls).__new__(cls, _args)
+            obj = super(AssocOp, cls).__new__(cls, *ordered(_args))
             obj._argset = _args
             return obj
 
@@ -524,13 +522,6 @@ def make_args(cls, expr):
         else:
             return frozenset([sympify(expr)])
 
-    # XXX: This should be cached on the object rather than using cacheit
-    # Maybe _argset can just be sorted in the constructor?
-    @property  # type: ignore
-    @cacheit
-    def args(self):
-        return tuple(ordered(self._argset))
-
     @staticmethod
     def _compare_pretty(a, b):
         return (str(a) > str(b)) - (str(a) < str(b))
diff --git a/sympy/functions/elementary/miscellaneous.py b/sympy/functions/elementary/miscellaneous.py
index 3e958c55f8..8466fe0ab5 100644
--- a/sympy/functions/elementary/miscellaneous.py
+++ b/sympy/functions/elementary/miscellaneous.py
@@ -1,6 +1,7 @@
 from sympy.core import Function, S, sympify
 from sympy.core.add import Add
 from sympy.core.containers import Tuple
+from sympy.core.compatibility import ordered
 from sympy.core.operations import LatticeOp, ShortCircuit
 from sympy.core.function import (Application, Lambda,
     ArgumentIndexError)
@@ -405,7 +406,7 @@ def __new__(cls, *args, **assumptions):
 
         # base creation
         _args = frozenset(args)
-        obj = Expr.__new__(cls, _args, **assumptions)
+        obj = Expr.__new__(cls, *ordered(_args), **assumptions)
         obj._argset = _args
         return obj

I haven't timed that to see how it compares.

Comment on lines 475 to 479
def _node_count(e):
if hasattr(e,'args'):
return 1 + sum(map(_node_count, e.args))
elif isinstance(e, int):
return 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only call this _node_count function if e is an instance of Basic. That should mean that it is guaranteed to have .args that are also all Basic. The hasattr above shouldn't be necessary and it shouldn't be possible for e to be an int. I think these are both workarounds covering up bugs.

Can you show examples where the hasattr and the elif etc are needed?

Copy link
Contributor Author

@kc611 kc611 Dec 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the stats module there are many instances of function objects being in the .args list (Formerly these were counted as 0 in the preorder traversal of e.count(Basic) ) . An example is the core test of stats\test\test_stochastic_process.py

sympy/stats/tests/test_stochastic_process.py:515: 
    assert P(X(t) < 1) == exp(-3*t)
sympy/stats/rv.py:809: in probability
    return Probability(condition, given_condition).doit(**kwargs)
sympy/stats/symbolic_probability.py:75: in doit
    return pspace(condition).probability(condition, given_condition,
sympy/stats/stochastic_process.py:53: in probability
    return self.process.probability(condition, given_condition, evaluate, **kwargs)
sympy/stats/stochastic_process_types.py:2104: in probability
    return _SubstituteRV._probability(condition, evaluate=evaluate, **kwargs)
sympy/stats/stochastic_process_types.py:1820: in _probability
    new_condition, new_givencondition = self._rvindexed_subs(condition, given_condition)
sympy/stats/stochastic_process_types.py:1755: in _rvindexed_subs
    expr = expr.subs(swapdict_expr)
sympy/core/basic.py:921: in subs
    k = list(ordered(sequence, default=False, keys=(
sympy/core/compatibility.py:589: in ordered
    d[f(a)].append(a)
sympy/core/basic.py:922: in <lambda>
    lambda x: -_nodes(x),
sympy/core/compatibility.py:492: in _nodes
    assert _node_count(e)==e.count(Basic)
sympy/core/compatibility.py:476: in _node_count
    return 1 + sum(map(_node_count, e.args))
sympy/core/compatibility.py:476: in _node_count
    return 1 + sum(map(_node_count, e.args))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

e = <bound method PoissonProcess.distribution of PoissonProcess(X, 3)>

    def _node_count(e):
>       return 1 + sum(map(_node_count, e.args))
E       AttributeError: 'function' object has no attribute 'args'

Also there was an instance of an int object in the core test unify\test\test_sympy.py . This is a singular occurence and probably a bug.

    def test_FiniteSet_complex():
        from sympy import FiniteSet
        a, b, c, x, y, z = symbols('a,b,c,x,y,z')
>       expr = FiniteSet(Basic(1, x), y, Basic(x, z))

sympy/unify/tests/test_sympy.py:127: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
sympy/sets/sets.py:1778: in __new__
    for i in reversed(list(ordered(args))):
sympy/core/compatibility.py:608: in ordered
    yield from d[k]
sympy/core/compatibility.py:589: in ordered
    d[f(a)].append(a)
sympy/core/compatibility.py:492: in _nodes
    assert _node_count(e)==e.count(Basic)
sympy/core/compatibility.py:476: in _node_count
    return 1 + sum(map(_node_count, e.args))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

e = 1

    def _node_count(e):
>       return 1 + sum(map(_node_count, e.args))
E       AttributeError: 'int' object has no attribute 'args'

Intrestingly I just found out returning 0 or 1 for either of the function orint objects didn't make any difference (all core tests passed in both cases). But the current e.count(Basic) returns them as 0 so probably these might be part of a bug or might be intended logic which e.count(Basic) covers up for by returning 0 for them, given that only instances of Basic are allowed in the .args of Basic

Copy link
Contributor Author

@kc611 kc611 Dec 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are also few instances of NoneType being returned in stats\test\test_stochastic_process.py

>       assert P(Eq(Y[3], 2), Eq(Y[1], 1) & TransitionMatrixOf(Y, TO)).round(3) == Float(0.375, 3)

sympy/stats/tests/test_stochastic_process.py:90: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
sympy/core/decorators.py:266: in _func
    return func(self, other)
sympy/logic/boolalg.py:70: in __and__
    return And(self, other)
sympy/core/operations.py:489: in __new__
    _args = frozenset(cls._new_args_filter(args))
sympy/logic/boolalg.py:685: in _new_args_filter
    for x in ordered(args):
sympy/core/compatibility.py:607: in ordered
    yield from d[k]
sympy/core/compatibility.py:588: in ordered
    d[f(a)].append(a)
sympy/core/compatibility.py:491: in _nodes
    assert _node_count(e)==e.count(Basic)
sympy/core/compatibility.py:476: in _node_count
    return 1 + sum(map(_node_count, e.args))
sympy/core/compatibility.py:476: in _node_count
    return 1 + sum(map(_node_count, e.args))
sympy/core/compatibility.py:476: in _node_count
    return 1 + sum(map(_node_count, e.args))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

e = None

    def _node_count(e):
>       return 1 + sum(map(_node_count, e.args))
E       AttributeError: 'NoneType' object has no attribute 'args'

Again returning these cases as 1 or 0 still passed the test but e.count(Basic) returned them as 0.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that something like #20078 (comment)

I think some part of this is being fixed in #20626

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why it's good that we don't cover over things like this so that these mistakes are picked up early.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I'll remove the check.

#20626 this will fix the bugs in stats but the int instance in unify still persists.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that test can just be changed to Basic(S(1), x).

Copy link
Contributor Author

@kc611 kc611 Dec 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright I made the necessary changes.

Basic(S(1), x)

I added this for now. Perhaps later we could add the ability to internally convert int instances to sympy's Integer() if passed like in the original test.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that most of the time the args are already Basic so sympifying them would be mostly unnecessary and would slow a lot of things down. It would be better if we had a way of testing this without actually having the checks at runtime.

@kc611
Copy link
Contributor Author

kc611 commented Dec 21, 2020

I haven't timed that to see how it compares.

It's considerably faster. Result for this example.

from sympy import *

xs = symbols('x:400')

b = True

for x in xs:
    b &= x

Upon running on current codebase :

$ python -m cProfile -s cumulative eg3.py
         19317080 function calls (18182213 primitive calls) in 9.703 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    577/1    0.008    0.000    9.703    9.703 {built-in method builtins.exec}
        1    0.001    0.001    9.703    9.703 eg3.py:1(<module>)
      401    0.001    0.000    9.132    0.023 decorators.py:254(_func)
      400    0.001    0.000    9.131    0.023 boolalg.py:68(__and__)
      400    0.021    0.000    9.130    0.023 operations.py:481(__new__)
966931/322845    1.618    0.000    8.349    0.000 compatibility.py:505(ordered)
322504/81896    0.242    0.000    6.671    0.000 cache.py:69(wrapper)
      400    0.035    0.000    4.594    0.011 boolalg.py:679(_new_args_filter)
      399    0.007    0.000    4.502    0.011 function.py:270(__new__)
84697/4498    0.026    0.000    4.502    0.001 sympify.py:92(sympify)
      399    0.004    0.000    4.491    0.011 sets.py:1958(<lambda>)
      399    0.100    0.000    4.487    0.011 sets.py:1766(__new__)
320466/320431    1.841    0.000    2.889    0.000 compatibility.py:314(default_sort_key)
      400    0.013    0.000    2.559    0.006 boolalg.py:452(binary_check_and_simplify)
      400    0.001    0.000    2.163    0.005 boolalg.py:456(<listcomp>)
      399    0.006    0.000    2.157    0.005 boolalg.py:172(binary_symbols)
      398    0.013    0.000    2.052    0.005 operations.py:529(args)
   320431    1.012    0.000    2.048    0.000 compatibility.py:483(_nodes)
       53    0.002    0.000    1.763    0.033 __init__.py:1(<module>)
  1924073    0.737    0.000    0.998    0.000 <frozen importlib._bootstrap>:389(parent)
  3204571    0.720    0.000    0.986    0.000 numbers.py:2279(__hash__)
   320396    0.305    0.000    0.563    0.000 compatibility.py:475(_node_count)
    554/1    0.003    0.000    0.562    0.562 <frozen importlib._bootstrap>:986(_find_and_load)
    554/1    0.002    0.000    0.562    0.562 <frozen importlib._bootstrap>:956(_find_and_load_unlocked)

After this Fix :

python -m cProfile -s cumulative eg3.py
         10546074 function calls (10053987 primitive calls) in 5.325 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    577/1    0.008    0.000    5.325    5.325 {built-in method builtins.exec}
        1    0.002    0.002    5.325    5.325 eg3.py:1(<module>)
      401    0.001    0.000    4.740    0.012 decorators.py:254(_func)
      400    0.001    0.000    4.739    0.012 boolalg.py:68(__and__)
      400    0.037    0.000    4.738    0.012 operations.py:481(__new__)
484546/162050    0.777    0.000    4.053    0.000 compatibility.py:505(ordered)
      400    0.035    0.000    2.533    0.006 boolalg.py:679(_new_args_filter)
       53    0.002    0.000    1.812    0.034 __init__.py:1(<module>)
160468/160433    0.918    0.000    1.446    0.000 compatibility.py:314(default_sort_key)
   160433    0.514    0.000    1.042    0.000 compatibility.py:483(_nodes)
    554/1    0.003    0.000    0.575    0.575 <frozen importlib._bootstrap>:986(_find_and_load)
    554/1    0.003    0.000    0.575    0.575 <frozen importlib._bootstrap>:956(_find_and_load_unlocked)
    529/1    0.003    0.000    0.575    0.575 <frozen importlib._bootstrap>:650(_load_unlocked)
    503/1    0.002    0.000    0.575    0.575 <frozen importlib._bootstrap_external>:777(exec_module)
    631/1    0.001    0.000    0.575    0.575 <frozen importlib._bootstrap>:211(_call_with_frames_removed)
   964084    0.370    0.000    0.501    0.000 <frozen importlib._bootstrap>:389(parent)
  1604591    0.356    0.000    0.491    0.000 numbers.py:2279(__hash__)
      400    0.013    0.000    0.483    0.001 boolalg.py:452(binary_check_and_simplify)
   160398    0.152    0.000    0.285    0.000 compatibility.py:475(_node_count)
161710/161299    0.134    0.000    0.279    0.000 cache.py:69(wrapper)

@kc611
Copy link
Contributor Author

kc611 commented Dec 26, 2020

To be closed and opened again for restarting tests after #20626 is merged and see if the underlying issues in stats are fixed.

See:
#20635 (comment)
#20635 (comment)

@czgdp1807
Copy link
Member

Restarted all jobs.

@kc611 kc611 closed this Jan 8, 2021
@kc611 kc611 reopened this Jan 8, 2021
@kc611
Copy link
Contributor Author

kc611 commented Jan 8, 2021

One of the failing test in current PR seems to be random ( similar to those which were caused by @cacheit previously)

The test is :

________________________________________________________________________________
______ sympy/assumptions/tests/test_sathandlers.py:test_UnevaluatedOnFree ______
Traceback (most recent call last):
  File "/home/runner/work/sympy/sympy/sympy/assumptions/tests/test_sathandlers.py", line 33, in test_UnevaluatedOnFree

The pytest stack trace is as follows :

       assert b.rcall(x) == UnevaluatedOnFree(Q.positive(x) | Q.negative(x))
E       assert UnevaluatedOnFree(Q.negative(x) | Q.positive(x)) == UnevaluatedOnFree(Q.positive(x) | Q.negative(x))
E        +  where UnevaluatedOnFree(Q.negative(x) | Q.positive(x)) = <bound method Basic.rcall of UnevaluatedOnFree(Q.negative | Q.positive)>(x)
E        +    where <bound method Basic.rcall of UnevaluatedOnFree(Q.negative | Q.positive)> = UnevaluatedOnFree(Q.negative | Q.positive).rcall
E        +  and   UnevaluatedOnFree(Q.positive(x) | Q.negative(x)) = UnevaluatedOnFree((Q.positive(x) | Q.negative(x)))
E        +    where Q.positive(x) = Q.positive(x)
E        +      where Q.positive = <sympy.assumptions.ask.AssumptionKeys object at 0x7f269da0b910>.positive
E        +    and   Q.negative(x) = Q.negative(x)
E        +      where Q.negative = <sympy.assumptions.ask.AssumptionKeys object at 0x7f269da0b910>.negative

sympy/assumptions/tests/test_sathandlers.py:33: AssertionError

@oscarbenjamin
Copy link
Collaborator

The failure I see is

$ pytest sympy/core/tests/test_basic.py::test_call
...
    
>       assert (Q.real & Q.positive).rcall(x) == Q.real(x) & Q.positive(x)
E       assert Q.positive(x) & Q.real(x) == (Q.real(x) & Q.positive(x))
E        +  where Q.positive(x) & Q.real(x) = <bound method Basic.rcall of Q.positive & Q.real>(x)
E        +    where <bound method Basic.rcall of Q.positive & Q.real> = (Q.real & Q.positive).rcall
E        +      where Q.real = <sympy.assumptions.ask.AssumptionKeys object at 0x7fd85060b520>.real
E        +      and   Q.positive = <sympy.assumptions.ask.AssumptionKeys object at 0x7fd85060b520>.positive
E        +  and   Q.real(x) = Q.real(x)
E        +    where Q.real = <sympy.assumptions.ask.AssumptionKeys object at 0x7fd85060b520>.real
E        +  and   Q.positive(x) = Q.positive(x)
E        +    where Q.positive = <sympy.assumptions.ask.AssumptionKeys object at 0x7fd85060b520>.positive

sympy/core/tests/test_basic.py:225: AssertionError

That seems to fail 50% of the time.

@kc611
Copy link
Contributor Author

kc611 commented Jan 11, 2021

The issue pointed out here #20635 (comment) seems to be arising from typecasting a list of arguments (which are always in same order) into And over here:

return type(expr_to_call)(*args)

Which ultimately leads us to here:

obj = super(AssocOp, cls).__new__(cls, _args)
obj._argset = _args
return obj

It almost seems like the newly added ordered() call is not working for arguments of those type. Hence the answer sometime results in Q.positive(x) & Q.real(x) instead of Q.real(x) & Q.positive(x). This #20635 (comment) issue is also feels similar since the same kind of arguments are involved.

@oscarbenjamin
Copy link
Collaborator

Yes, ordered does not handle these:

# t.py 
from sympy import *
from sympy.abc import x

s = frozenset([Q.real(x), Q.positive(x)])

print(list(ordered(s)))

That gives:

$ python t.py 
[Q.real(x), Q.positive(x)]
$ python t.py 
[Q.positive(x), Q.real(x)]

The result is different in different processes because it depends on set ordering.

I guess that the problem is this:

In [1]: p1 = Q.positive(x)

In [2]: p2 = Q.real(x)

In [3]: type(p1)
Out[3]: sympy.assumptions.assume.AppliedPredicate

In [4]: type(p1) == type(p2)
Out[4]: True

In [5]: p1.args == p2.args
Out[5]: True

In general two Basic instances of the same class with the same args should be equal.

I think that the "new" design for Predicate should fix this although Q.positive and Q.real are not yet migrated over to it. See #20656

@JSS95
Copy link
Collaborator

JSS95 commented Jan 12, 2021

If the problem comes from that Q.positive(x).args == Q.real(x).args, it's because of args overriding in Predicate which is not yet fixed.

@oscarbenjamin
Copy link
Collaborator

Yes, it is because of the args.

@JSS95
Copy link
Collaborator

JSS95 commented Jan 12, 2021

After every old predicates are migrated, I will remove args overriding.

@JSS95
Copy link
Collaborator

JSS95 commented Jan 22, 2021

I'm afraid that fixing args turned out to be very difficult. It broke many core methods on AppliedPredicate, such as atoms() or xreplace(). I suggest adding .func check in Basic.__eq__ instead.

@oscarbenjamin
Copy link
Collaborator

I'm afraid that fixing args turned out to be very difficult. It broke many core methods on AppliedPredicate, such as atoms() or xreplace(). I suggest adding .func check in Basic.__eq__ instead.

It shouldn't be necessary to change Basic.__eq__. Can you open a separate issue to discuss changing the args and clarify what the problems are?

@czgdp1807
Copy link
Member

Some tests are failing on this.

@JSS95
Copy link
Collaborator

JSS95 commented Apr 16, 2021

@oscarbenjamin Does this look OK?

@oscarbenjamin
Copy link
Collaborator

It looks good to me although it shouldn't be listed as fixing the issue because parse_expr can still be improved to use a linear-time algorithm method for chained associative operators.

@JSS95
Copy link
Collaborator

JSS95 commented Apr 17, 2021

Then I'll edit the OP and merge this.

@JSS95 JSS95 merged commit 6d0042b into sympy:master Apr 17, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants