From a96e5074486e2c1284f1e4e4dc0332d51a694db8 Mon Sep 17 00:00:00 2001 From: qthequartermasterman Date: Fri, 4 Jun 2021 17:21:43 -0500 Subject: [PATCH 1/9] Override __bool__ in Relational, so that it returns whatever the boolean evaluation is, instead of always True. Resolves #312 --- symengine/lib/symengine_wrapper.pyx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/symengine/lib/symengine_wrapper.pyx b/symengine/lib/symengine_wrapper.pyx index 4dde6531..4229c083 100644 --- a/symengine/lib/symengine_wrapper.pyx +++ b/symengine/lib/symengine_wrapper.pyx @@ -1502,6 +1502,14 @@ class Relational(Boolean): def is_Relational(self): return True + def __bool__(self): + if len(self.free_symbols): + # If there are any free symbols, then boolean evaluation is ambiguous in most cases. Throw a Type Error + raise TypeError(f'Relational with free symbols cannot be cast as bool: {self}') + else: + simplification = self.simplify() + return bool(simplification) + Rel = Relational From 290f916cd6836f5b798d0866b8877cabd2d8bbf3 Mon Sep 17 00:00:00 2001 From: qthequartermasterman Date: Sat, 5 Jun 2021 16:10:30 -0500 Subject: [PATCH 2/9] Update test_eval.py Fixed incorrect evaluation test. To get rid of a sin(x)**2, we need to subtract sin(x)**2, not x**2. --- symengine/tests/test_eval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/symengine/tests/test_eval.py b/symengine/tests/test_eval.py index 1ea2b51f..c1cca41f 100644 --- a/symengine/tests/test_eval.py +++ b/symengine/tests/test_eval.py @@ -16,7 +16,7 @@ def test_eval_double2(): x = Symbol("x") e = sin(x)**2 + sqrt(2) raises(RuntimeError, lambda: e.n(real=True)) - assert abs(e.n() - x**2 - 1.414) < 1e-3 + assert abs(e.n() - sin(x)**2.0 - 1.414) < 1e-3 def test_n(): x = Symbol("x") From 8709957b9e592d9c4adf3e68fb5360fdc0c50168 Mon Sep 17 00:00:00 2001 From: qthequartermasterman Date: Mon, 7 Jun 2021 14:36:35 -0500 Subject: [PATCH 3/9] Improved speed of boolean evaluation. --- symengine/lib/symengine_wrapper.pyx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/symengine/lib/symengine_wrapper.pyx b/symengine/lib/symengine_wrapper.pyx index 4229c083..8f0c779c 100644 --- a/symengine/lib/symengine_wrapper.pyx +++ b/symengine/lib/symengine_wrapper.pyx @@ -1503,12 +1503,24 @@ class Relational(Boolean): return True def __bool__(self): - if len(self.free_symbols): + # We will narrow down the boolean value of our relational with some simple checks + lhs, rhs = self.args + + # Two expressions are equal if their difference is equal to 0. + # If the expand method will not cancel out free symbols in the given expression, then this + # will throw a TypeError. + difference = (lhs - rhs).expand().evalf() + float_threshold = 1e-9 # Maximum difference we will allow before doing the full simplification + + if len(difference.free_symbols): # If there are any free symbols, then boolean evaluation is ambiguous in most cases. Throw a Type Error raise TypeError(f'Relational with free symbols cannot be cast as bool: {self}') + elif difference > float_threshold: + # If the float evaluation is larger than the threshold, we can skip the full simplification. + return False else: - simplification = self.simplify() - return bool(simplification) + # If the float evaluation is smaller than the threshold, then we will need a full simplification. + return bool(self.simplify()) Rel = Relational From 4090967b363761187d71d2da8dc5bdfa7c8384bf Mon Sep 17 00:00:00 2001 From: qthequartermasterman Date: Mon, 7 Jun 2021 14:37:10 -0500 Subject: [PATCH 4/9] Added tests for Equality --- symengine/tests/CMakeLists.txt | 1 + symengine/tests/test_relationals.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 symengine/tests/test_relationals.py diff --git a/symengine/tests/CMakeLists.txt b/symengine/tests/CMakeLists.txt index ebd4dfaa..ee04a9be 100644 --- a/symengine/tests/CMakeLists.txt +++ b/symengine/tests/CMakeLists.txt @@ -9,6 +9,7 @@ install(FILES __init__.py test_matrices.py test_ntheory.py test_printing.py + test_relationals.py test_sage.py test_series_expansion.py test_sets.py diff --git a/symengine/tests/test_relationals.py b/symengine/tests/test_relationals.py new file mode 100644 index 00000000..f1dddf41 --- /dev/null +++ b/symengine/tests/test_relationals.py @@ -0,0 +1,45 @@ +from symengine.utilities import raises +from symengine import (Symbol, sympify, Eq) + + +def test_equals_constants(): + assert bool(Eq(3, 3)) + assert bool(Eq(4, 2**2)) + + # Short and long are symbolically equivalent, but sufficiently different in form that expand() and evalf() does not + # catch it. Despite the float evaluation differing ever so slightly, ideally, our equality should still catch + # symbolically equal expressions. + short = sympify('(3/2)*sqrt(11 + sqrt(21))') + long = sympify('sqrt((33/8 + (1/24)*sqrt(27)*sqrt(63))**2 + ((3/8)*sqrt(27) + (-1/8)*sqrt(63))**2)') + assert bool(Eq(short, short)) + assert bool(Eq(long, long)) + assert bool(Eq(short, long)) + + +def test_not_equals_constants(): + assert not bool(Eq(3, 4)) + assert not bool(Eq(4, 4-.000000001)) + + +def test_equals_symbols(): + x = Symbol("x") + y = Symbol("y") + assert bool(Eq(x, x)) + assert bool(Eq(x**2, x*x)) + assert bool(Eq(x*y, y*x)) + + +def test_not_equals_symbols(): + x = Symbol("x") + y = Symbol("y") + assert not bool(Eq(x, x+1)) + assert not bool(Eq(x**2, x**2+1)) + assert not bool(Eq(x * y, y * x+1)) + + +def test_not_equals_symbols_raise_typeerror(): + x = Symbol("x") + y = Symbol("y") + raises(TypeError, lambda: bool(Eq(x, 1))) + raises(TypeError, lambda: bool(Eq(x, y))) + raises(TypeError, lambda: bool(Eq(x**2, x))) \ No newline at end of file From f81c74ab7ae5bf5e6f9b0be17b05863af8725980 Mon Sep 17 00:00:00 2001 From: qthequartermasterman Date: Mon, 7 Jun 2021 15:34:59 -0500 Subject: [PATCH 5/9] Added Tests for Unequality, Less than, and Greater Than. --- symengine/tests/test_relationals.py | 90 ++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/symengine/tests/test_relationals.py b/symengine/tests/test_relationals.py index f1dddf41..769c690a 100644 --- a/symengine/tests/test_relationals.py +++ b/symengine/tests/test_relationals.py @@ -1,40 +1,70 @@ from symengine.utilities import raises -from symengine import (Symbol, sympify, Eq) +from symengine import (Symbol, sympify, Eq, Ne, Lt, Le, Ge, Gt, sqrt, pi) + + +def assert_equal(x, y): + """Asserts that x and y are equal. This will test Equality, Unequality, LE, and GE classes.""" + assert bool(Eq(x, y)) + assert not bool(Ne(x, y)) + assert bool(Ge(x, y)) + assert bool(Le(x, y)) + + +def assert_not_equal(x, y): + """Asserts that x and y are not equal. This will test Equality and Unequality""" + assert not bool(Eq(x, y)) + assert bool(Ne(x, y)) + + +def assert_less_than(x, y): + """Asserts that x is less than y. This will test Le, Lt, Ge, Gt classes.""" + assert bool(Le(x, y)) + assert bool(Lt(x, y)) + assert not bool(Ge(x, y)) + assert not bool(Gt(x, y)) + + +def assert_greater_than(x, y): + """Asserts that x is greater than y. This will test Le, Lt, Ge, Gt classes.""" + assert not bool(Le(x, y)) + assert not bool(Lt(x, y)) + assert bool(Ge(x, y)) + assert bool(Gt(x, y)) def test_equals_constants(): - assert bool(Eq(3, 3)) - assert bool(Eq(4, 2**2)) + assert_equal(3, 3) + assert_equal(4, 2 ** 2) # Short and long are symbolically equivalent, but sufficiently different in form that expand() and evalf() does not # catch it. Despite the float evaluation differing ever so slightly, ideally, our equality should still catch # symbolically equal expressions. short = sympify('(3/2)*sqrt(11 + sqrt(21))') long = sympify('sqrt((33/8 + (1/24)*sqrt(27)*sqrt(63))**2 + ((3/8)*sqrt(27) + (-1/8)*sqrt(63))**2)') - assert bool(Eq(short, short)) - assert bool(Eq(long, long)) - assert bool(Eq(short, long)) + assert_equal(short, short) + assert_equal(long, long) + assert_equal(short, long) def test_not_equals_constants(): - assert not bool(Eq(3, 4)) - assert not bool(Eq(4, 4-.000000001)) + assert_not_equal(3, 4) + assert_not_equal(4, 4 - .000000001) def test_equals_symbols(): x = Symbol("x") y = Symbol("y") - assert bool(Eq(x, x)) - assert bool(Eq(x**2, x*x)) - assert bool(Eq(x*y, y*x)) + assert_equal(x, x) + assert_equal(x ** 2, x * x) + assert_equal(x * y, y * x) def test_not_equals_symbols(): x = Symbol("x") y = Symbol("y") - assert not bool(Eq(x, x+1)) - assert not bool(Eq(x**2, x**2+1)) - assert not bool(Eq(x * y, y * x+1)) + assert_not_equal(x, x + 1) + assert_not_equal(x ** 2, x ** 2 + 1) + assert_not_equal(x * y, y * x + 1) def test_not_equals_symbols_raise_typeerror(): @@ -42,4 +72,34 @@ def test_not_equals_symbols_raise_typeerror(): y = Symbol("y") raises(TypeError, lambda: bool(Eq(x, 1))) raises(TypeError, lambda: bool(Eq(x, y))) - raises(TypeError, lambda: bool(Eq(x**2, x))) \ No newline at end of file + raises(TypeError, lambda: bool(Eq(x ** 2, x))) + + +def test_less_than_constants(): + assert_less_than(1, 2) + assert_less_than(sqrt(2), 2) + assert_less_than(-1, 1) + assert_less_than(3.14, pi) + + +def test_greater_than_constants(): + assert_greater_than(2, 1) + assert_greater_than(2, sqrt(2)) + assert_greater_than(1, -1, ) + assert_greater_than(pi, 3.14) + + +def test_less_than_raises_typeerror(): + x = Symbol("x") + y = Symbol("y") + raises(TypeError, lambda: bool(Lt(x, 1))) + raises(TypeError, lambda: bool(Lt(x, y))) + raises(TypeError, lambda: bool(Lt(x ** 2, x))) + + +def test_greater_than_raises_typeerror(): + x = Symbol("x") + y = Symbol("y") + raises(TypeError, lambda: bool(Gt(x, 1))) + raises(TypeError, lambda: bool(Gt(x, y))) + raises(TypeError, lambda: bool(Gt(x ** 2, x))) From afa49250ad7a608da9ab527cc647931d630695fd Mon Sep 17 00:00:00 2001 From: qthequartermasterman Date: Mon, 7 Jun 2021 15:35:17 -0500 Subject: [PATCH 6/9] Fixed boolean evaluation for Less Than and Greater Than. --- symengine/lib/symengine_wrapper.pyx | 33 +++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/symengine/lib/symengine_wrapper.pyx b/symengine/lib/symengine_wrapper.pyx index 8f0c779c..9ab70867 100644 --- a/symengine/lib/symengine_wrapper.pyx +++ b/symengine/lib/symengine_wrapper.pyx @@ -1504,20 +1504,16 @@ class Relational(Boolean): def __bool__(self): # We will narrow down the boolean value of our relational with some simple checks - lhs, rhs = self.args - - # Two expressions are equal if their difference is equal to 0. + # Get the Left- and Right-hand-sides of the relation, since two expressions are equal if their difference + # is equal to 0. # If the expand method will not cancel out free symbols in the given expression, then this # will throw a TypeError. + lhs, rhs = self.args difference = (lhs - rhs).expand().evalf() - float_threshold = 1e-9 # Maximum difference we will allow before doing the full simplification if len(difference.free_symbols): # If there are any free symbols, then boolean evaluation is ambiguous in most cases. Throw a Type Error raise TypeError(f'Relational with free symbols cannot be cast as bool: {self}') - elif difference > float_threshold: - # If the float evaluation is larger than the threshold, we can skip the full simplification. - return False else: # If the float evaluation is smaller than the threshold, then we will need a full simplification. return bool(self.simplify()) @@ -1544,6 +1540,29 @@ class Equality(Relational): def is_Equality(self): return True + def __bool__(self): + # We override __bool__ in Equality just for some an additional check (for speed) + # that does not easily generalize to other relationals. + # + # We will narrow down the boolean value of our relational with some simple checks + # Get the Left- and Right-hand-sides of the relation, since two expressions are equal if their difference + # is equal to 0. + # If the expand method will not cancel out free symbols in the given expression, then this + # will throw a TypeError. + lhs, rhs = self.args + difference = (lhs - rhs).expand().evalf() + float_threshold = 1e-9 # Maximum difference we will allow before doing the full simplification + + if len(difference.free_symbols): + # If there are any free symbols, then boolean evaluation is ambiguous in most cases. Throw a Type Error + raise TypeError(f'Relational with free symbols cannot be cast as bool: {self}') + elif difference > float_threshold: + # If the float evaluation is larger than the threshold, we can skip the full simplification. + return False + else: + # If the float evaluation is smaller than the threshold, then we will need a full simplification. + return bool(self.simplify()) + func = __class__ From feaeca0c17b319303b6080177f6523fcd77c216c Mon Sep 17 00:00:00 2001 From: qthequartermasterman Date: Mon, 7 Jun 2021 21:55:43 -0500 Subject: [PATCH 7/9] Include workaround for environments without sympy. --- symengine/lib/symengine_wrapper.pyx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/symengine/lib/symengine_wrapper.pyx b/symengine/lib/symengine_wrapper.pyx index 9ab70867..b184a68f 100644 --- a/symengine/lib/symengine_wrapper.pyx +++ b/symengine/lib/symengine_wrapper.pyx @@ -1516,7 +1516,13 @@ class Relational(Boolean): raise TypeError(f'Relational with free symbols cannot be cast as bool: {self}') else: # If the float evaluation is smaller than the threshold, then we will need a full simplification. - return bool(self.simplify()) + # If sympy is not present, then we can do a workaround with evalf. This work around is not as precise as + # using sympy's simplification. + try: + return bool(self.simplify()) + except ImportError: + relational_type = type(self) + return bool(relational_type(difference, 0).evalf()) Rel = Relational @@ -1556,12 +1562,17 @@ class Equality(Relational): if len(difference.free_symbols): # If there are any free symbols, then boolean evaluation is ambiguous in most cases. Throw a Type Error raise TypeError(f'Relational with free symbols cannot be cast as bool: {self}') - elif difference > float_threshold: + elif abs(difference) > float_threshold: # If the float evaluation is larger than the threshold, we can skip the full simplification. return False else: # If the float evaluation is smaller than the threshold, then we will need a full simplification. - return bool(self.simplify()) + # If sympy is not present, then we can do a workaround with evalf. This work around is not as precise as + # using sympy's simplification. + try: + return bool(self.simplify()) + except ImportError: + return bool(Eq(difference, 0).evalf()) func = __class__ From 1d4d0e9509e9d97aeb54d508145b41a2176727bf Mon Sep 17 00:00:00 2001 From: qthequartermasterman Date: Tue, 8 Jun 2021 11:23:01 -0500 Subject: [PATCH 8/9] Fix tests so that they will accept a ValueError for hard relational evaluations. --- symengine/tests/test_relationals.py | 55 +++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/symengine/tests/test_relationals.py b/symengine/tests/test_relationals.py index 769c690a..81b2e483 100644 --- a/symengine/tests/test_relationals.py +++ b/symengine/tests/test_relationals.py @@ -1,6 +1,14 @@ from symengine.utilities import raises from symengine import (Symbol, sympify, Eq, Ne, Lt, Le, Ge, Gt, sqrt, pi) +from unittest.case import SkipTest + +try: + import sympy + HAVE_SYMPY = True +except ImportError: + HAVE_SYMPY = False + def assert_equal(x, y): """Asserts that x and y are equal. This will test Equality, Unequality, LE, and GE classes.""" @@ -32,18 +40,23 @@ def assert_greater_than(x, y): assert bool(Gt(x, y)) -def test_equals_constants(): +def test_equals_constants_easy(): assert_equal(3, 3) assert_equal(4, 2 ** 2) - # Short and long are symbolically equivalent, but sufficiently different in form that expand() and evalf() does not - # catch it. Despite the float evaluation differing ever so slightly, ideally, our equality should still catch - # symbolically equal expressions. + +def test_equals_constants_hard(): + # Short and long are symbolically equivalent, but sufficiently different in form that expand() does not + # catch it. Ideally, our equality should still catch these, but until symengine supports as robust simplification as + # sympy, we can forgive failing, as long as it raises a ValueError short = sympify('(3/2)*sqrt(11 + sqrt(21))') long = sympify('sqrt((33/8 + (1/24)*sqrt(27)*sqrt(63))**2 + ((3/8)*sqrt(27) + (-1/8)*sqrt(63))**2)') assert_equal(short, short) assert_equal(long, long) - assert_equal(short, long) + if HAVE_SYMPY: + assert_equal(short, long) + else: + raises(ValueError, lambda: bool(Eq(short, long))) def test_not_equals_constants(): @@ -75,18 +88,38 @@ def test_not_equals_symbols_raise_typeerror(): raises(TypeError, lambda: bool(Eq(x ** 2, x))) -def test_less_than_constants(): +def test_less_than_constants_easy(): assert_less_than(1, 2) - assert_less_than(sqrt(2), 2) assert_less_than(-1, 1) - assert_less_than(3.14, pi) + + +def test_less_than_constants_hard(): + # Each of the below pairs are distinct numbers, with the one on the left less than the one on the right. + # Ideally, Less-than will catch this when evaluated, but until symengine has a more robust simplification, + # we can forgive a failure to evaluate as long as it raises a ValueError. + if HAVE_SYMPY: + assert_less_than(sqrt(2), 2) + assert_less_than(3.14, pi) + else: + raises(ValueError, lambda: bool(Lt(sqrt(2), 2))) + raises(ValueError, lambda: bool(Lt(3.14, pi))) def test_greater_than_constants(): assert_greater_than(2, 1) - assert_greater_than(2, sqrt(2)) - assert_greater_than(1, -1, ) - assert_greater_than(pi, 3.14) + assert_greater_than(1, -1) + + +def test_greater_than_constants_hard(): + # Each of the below pairs are distinct numbers, with the one on the left less than the one on the right. + # Ideally, Greater-than will catch this when evaluated, but until symengine has a more robust simplification, + # we can forgive a failure to evaluate as long as it raises a ValueError. + if HAVE_SYMPY: + assert_greater_than(2, sqrt(2)) + assert_greater_than(pi, 3.14) + else: + raises(ValueError, lambda: bool(Gt(2, sqrt(2)))) + raises(ValueError, lambda: bool(Gt(pi, 3.14))) def test_less_than_raises_typeerror(): From cf784eb94db0ed0f94ac62fbbc9ca0cdbc42c5e7 Mon Sep 17 00:00:00 2001 From: qthequartermasterman Date: Tue, 8 Jun 2021 11:23:33 -0500 Subject: [PATCH 9/9] Eliminated all approximations in Relational boolean evaluation. Raise a ValueError if simplification is unclear. --- symengine/lib/symengine_wrapper.pyx | 44 +++++++---------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/symengine/lib/symengine_wrapper.pyx b/symengine/lib/symengine_wrapper.pyx index b184a68f..a4692edb 100644 --- a/symengine/lib/symengine_wrapper.pyx +++ b/symengine/lib/symengine_wrapper.pyx @@ -1509,20 +1509,24 @@ class Relational(Boolean): # If the expand method will not cancel out free symbols in the given expression, then this # will throw a TypeError. lhs, rhs = self.args - difference = (lhs - rhs).expand().evalf() + difference = (lhs - rhs).expand() if len(difference.free_symbols): # If there are any free symbols, then boolean evaluation is ambiguous in most cases. Throw a Type Error raise TypeError(f'Relational with free symbols cannot be cast as bool: {self}') else: - # If the float evaluation is smaller than the threshold, then we will need a full simplification. - # If sympy is not present, then we can do a workaround with evalf. This work around is not as precise as - # using sympy's simplification. + # Instantiating relationals that are obviously True or False (according to symengine) will automatically + # simplify to BooleanTrue or BooleanFalse + relational_type = type(self) + simplified = relational_type(difference, S.Zero) + if isinstance(simplified, BooleanAtom): + return bool(simplified) + # If we still cannot determine whether or not the relational is true, then we can either outsource the + # evaluation to sympy (if available) or raise a ValueError expressing that the evaluation is unclear. try: return bool(self.simplify()) except ImportError: - relational_type = type(self) - return bool(relational_type(difference, 0).evalf()) + raise ValueError(f'Boolean evaluation is unclear for relational: {self}') Rel = Relational @@ -1546,34 +1550,6 @@ class Equality(Relational): def is_Equality(self): return True - def __bool__(self): - # We override __bool__ in Equality just for some an additional check (for speed) - # that does not easily generalize to other relationals. - # - # We will narrow down the boolean value of our relational with some simple checks - # Get the Left- and Right-hand-sides of the relation, since two expressions are equal if their difference - # is equal to 0. - # If the expand method will not cancel out free symbols in the given expression, then this - # will throw a TypeError. - lhs, rhs = self.args - difference = (lhs - rhs).expand().evalf() - float_threshold = 1e-9 # Maximum difference we will allow before doing the full simplification - - if len(difference.free_symbols): - # If there are any free symbols, then boolean evaluation is ambiguous in most cases. Throw a Type Error - raise TypeError(f'Relational with free symbols cannot be cast as bool: {self}') - elif abs(difference) > float_threshold: - # If the float evaluation is larger than the threshold, we can skip the full simplification. - return False - else: - # If the float evaluation is smaller than the threshold, then we will need a full simplification. - # If sympy is not present, then we can do a workaround with evalf. This work around is not as precise as - # using sympy's simplification. - try: - return bool(self.simplify()) - except ImportError: - return bool(Eq(difference, 0).evalf()) - func = __class__