Skip to content

Commit

Permalink
Add Between, correct some types... (#1282)
Browse files Browse the repository at this point in the history
Some flycheck linting done on the `mathics.core.testing_expressions.equality_inequality` module.

Between is used the Biology, Chemistry, and CodeInspector packages among others.

---------

Co-authored-by: Juan Mauricio Matera <[email protected]>
  • Loading branch information
rocky and mmatera authored Jan 15, 2025
1 parent a2c14ad commit e0755b5
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGES
New Builtins
++++++++++++

* ``Between``
* ``CheckAbort``
* ``FileNameDrop``
* ``SetEnvironment``
Expand Down
198 changes: 134 additions & 64 deletions mathics/builtin/testing_expressions/equality_inequality.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
Equality and Inequality
"""

from typing import Any, Optional
from abc import ABC
from typing import Any, Optional, Union

import sympy

from mathics.builtin.numbers.constants import mp_convert_constant
from mathics.core.atoms import COMPARE_PREC, Integer, Integer1, Number, String
from mathics.core.atoms import COMPARE_PREC, Number, String
from mathics.core.attributes import (
A_FLAT,
A_NUMERIC_FUNCTION,
Expand All @@ -17,24 +18,18 @@
A_PROTECTED,
)
from mathics.core.builtin import Builtin, InfixOperator, SympyFunction
from mathics.core.convert.expression import to_expression, to_numeric_args
from mathics.core.convert.expression import to_numeric_args
from mathics.core.evaluation import Evaluation
from mathics.core.expression import Expression
from mathics.core.expression_predefined import (
MATHICS3_COMPLEX_INFINITY,
MATHICS3_INFINITY,
MATHICS3_NEG_INFINITY,
)
from mathics.core.expression_predefined import MATHICS3_INFINITY, MATHICS3_NEG_INFINITY
from mathics.core.number import dps
from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolList, SymbolTrue
from mathics.core.symbols import Symbol, SymbolFalse, SymbolList, SymbolTrue
from mathics.core.systemsymbols import (
SymbolAnd,
SymbolDirectedInfinity,
SymbolExactNumberQ,
SymbolInequality,
SymbolInfinity,
SymbolMaxExtraPrecision,
SymbolMaxPrecision,
SymbolSign,
)
from mathics.eval.nevaluator import eval_N
from mathics.eval.numerify import numerify
Expand All @@ -50,49 +45,60 @@
}


class _InequalityOperator(InfixOperator):
class _InequalityOperator(InfixOperator, ABC):
"""
A class for builtin functions with element inequality
comparisons in a chain e.g. a != b != c compares a != b and b !=
c.
"""

grouping = "NonAssociative"

@staticmethod
def numerify_args(items, evaluation) -> list:
items_sequence = items.get_sequence()
def numerify_args(elements, evaluation: Evaluation) -> Union[list, tuple]:
element_sequence = elements.get_sequence()
all_numeric = all(
item.is_numeric(evaluation) and item.get_precision() is None
for item in items_sequence
for item in element_sequence
)

# All expressions are numeric but exact and they are not all numbers,
if all_numeric and any(not isinstance(item, Number) for item in items_sequence):
if all_numeric and any(
not isinstance(item, Number) for item in element_sequence
):
# so apply N and compare them.
items = items_sequence
items = element_sequence
n_items = []
for item in items:
if not isinstance(item, Number):
item = eval_N(item, evaluation, SymbolMaxExtraPrecision)
n_items.append(item)
items = n_items
else:
items = to_numeric_args(items, evaluation)
items = to_numeric_args(elements, evaluation)
return items


class _ComparisonOperator(_InequalityOperator):
"Compares arguments in a chain e.g. a < b < c compares a < b and b < c."
class _ComparisonOperator(_InequalityOperator, ABC):
"""
A class for builtin functions with element comparisons in a
chain e.g. a < b < c compares a < b and b < c.
"""

def eval(self, items, evaluation):
"%(name)s[items___]"
items_sequence = items.get_sequence()
if len(items_sequence) <= 1:
def eval(self, elements, evaluation: Evaluation):
"%(name)s[elements___]"
elements_sequence = elements.get_sequence()
if len(elements_sequence) <= 1:
return SymbolTrue
items = self.numerify_args(items, evaluation)
elements = self.numerify_args(elements, evaluation)
wanted = operators[self.get_name()]
if isinstance(items[-1], String):
if isinstance(elements[-1], String):
return None
for i in range(len(items) - 1):
x = items[i]
for i in range(len(elements) - 1):
x = elements[i]
if isinstance(x, String):
return None
y = items[i + 1]
y = elements[i + 1]
c = do_cmp(x, y)
if c is None:
return
Expand All @@ -102,8 +108,11 @@ def eval(self, items, evaluation):
return SymbolTrue


class _EqualityOperator(_InequalityOperator):
"Compares all pairs e.g. a == b == c compares a == b, b == c, and a == c."
class _EqualityOperator(_InequalityOperator, ABC):
"""
A class for builtin functions with element equality in a
chain e.g. a == b == c compares a == b and b == c.
"""

@staticmethod
def get_pairs(args):
Expand Down Expand Up @@ -151,6 +160,15 @@ def infty_equal(self, lhs, rhs, max_extra_prec=None) -> Optional[bool]:
# DirectedInfinity with more than two elements cannot be compared here...
return None

# Below, we default to the method used for equality builtin functions.
# Inequality builtin functions will redefine this method.
@staticmethod
def operator_sense(value) -> bool:
"""function used to check whether `value` is the right Boolean-valued
sense needed for a particluar equality or inequality builtin function.
"""
return bool(value)

def sympy_equal(self, lhs, rhs, max_extra_prec=None) -> Optional[bool]:
try:
lhs_sympy = lhs.to_sympy(evaluate=True, prec=COMPARE_PREC)
Expand Down Expand Up @@ -209,28 +227,28 @@ def equal2(self, lhs: Any, rhs: Any, max_extra_prec=None) -> Optional[bool]:
return c
return None

def eval(self, items, evaluation):
"%(name)s[items___]"
items_sequence = items.get_sequence()
n = len(items_sequence)
def eval(self, elements, evaluation: Evaluation):
"%(name)s[elements___]"
elements_sequence = elements.get_sequence()
n = len(elements_sequence)
if n <= 1:
return SymbolTrue
is_exact_vals = [
Expression(SymbolExactNumberQ, arg).evaluate(evaluation)
for arg in items_sequence
for arg in elements_sequence
]
if not all(val is SymbolTrue for val in is_exact_vals):
return self.eval_other(items, evaluation)
args = self.numerify_args(items, evaluation)
return self.eval_other(elements, evaluation)
args = self.numerify_args(elements, evaluation)
for x, y in self.get_pairs(args):
c = do_cplx_equal(x, y)
if c is None:
return
if not self._op(c):
if not self.operator_sense(c):
return SymbolFalse
return SymbolTrue

def eval_other(self, args, evaluation):
def eval_other(self, args, evaluation: Evaluation):
"%(name)s[args___?(!ExactNumberQ[#]&)]"

args = args.get_sequence()
Expand All @@ -246,7 +264,7 @@ def eval_other(self, args, evaluation):
c = self.equal2(x, y, max_extra_prec)
if c is None:
return
if not self._op(c):
if not self.operator_sense(c):
return SymbolFalse
return SymbolTrue

Expand All @@ -256,7 +274,15 @@ class _MinMax(Builtin):
A_FLAT | A_NUMERIC_FUNCTION | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED
)

def eval(self, items, evaluation):
# "sense" should be either 1 for Maximum or -1 for Minimum
# This field is used to in comparison if figure out
# maximum/minimum sense.
# Below we default to value used for the Max builtin function
# The Min builtin function will redefine this.

sense = 1

def eval(self, items, evaluation: Evaluation):
"%(name)s[items___]"
if hasattr(items, "flatten_with_respect_to_head"):
items = items.flatten_with_respect_to_head(SymbolList)
Expand Down Expand Up @@ -313,6 +339,51 @@ def pairs(elements):
return to_sympy(expr, **kwargs)


class Between(Builtin):
"""
<url>
:WMA link:
https://reference.wolfram.com/language/ref/Between.html</url>
<dl>
<dt>'Between'[$x$, {$min$, $max$}]
<dd>equivalent to $min$ <= $x$ <= $max$.
<dt>'Between[$x$, { {$min1$, $max1$}, {$min2$, $max2$}, ...]'
<dd>equivalent to $min1$ <= $x$ <= $max1$' || $min2$ <= $x$ <= $max2$ ...
<dt>'Between[$range$]'
<dd>operator form that yields 'Between'[$x$, $range$] when applied to expression $x$.
</dl>
Check that 6 is in range 4..10:
>> Between[6, {4, 10}]
= True
Same as above in operator form:
>> Between[{4, 10}][6]
= True
'Between' works with irrational numbers:
>> Between[2, {E, Pi}]
= False
If more than an interval is given, 'Between' returns 'True' if $x$ belongs \\
to one of them:
>> {Between[3, {1, 2}, {4, 6}], Between[5, {1, 2}, {4, 6}]}
= {False, True}
"""

attributes = A_PROTECTED

rules = {
"Between[x_, {min_, max_}]": "min <= x <= max", # FIXME add error checking
"Between[x_, ranges__List]": "Module[{range},Do[If[Between[x,range], Return[True]],{range, {ranges}}]===True]",
"Between[range_List][x_]": "Between[x, range]", # operator form
}

summary_text = "test if value or values are in range"


class BooleanQ(Builtin):
"""
<url>
Expand Down Expand Up @@ -477,8 +548,8 @@ def get_pairs(args):
yield (args[i], args[i + 1])

@staticmethod
def _op(x):
return x
def operator_sense(value):
return value


class Greater(_ComparisonOperator, _SympyComparison):
Expand Down Expand Up @@ -555,10 +626,10 @@ class Inequality(Builtin):
}
summary_text = "chain of inequalities"

def eval(self, items, evaluation):
"Inequality[items___]"
def eval(self, elements, evaluation: Evaluation):
"Inequality[elements___]"

elements = numerify(items, evaluation).get_sequence()
elements = numerify(elements, evaluation).get_sequence()
count = len(elements)
if count == 1:
return SymbolTrue
Expand Down Expand Up @@ -657,8 +728,7 @@ class Max(_MinMax):
= Max[2, a, b]
"""

sense = 1
summary_text = "the maximum value"
summary_text = "get maximum value"


class Min(_MinMax):
Expand Down Expand Up @@ -692,7 +762,7 @@ class Min(_MinMax):
"""

sense = -1
summary_text = "the minimum value"
summary_text = "get minimum value"


class SameQ(_ComparisonOperator):
Expand Down Expand Up @@ -746,14 +816,14 @@ class SameQ(_ComparisonOperator):

summary_text = "literal symbolic identity"

def eval_list(self, items, evaluation):
"%(name)s[items___]"
items_sequence = items.get_sequence()
if len(items_sequence) <= 1:
def eval_list(self, elements, evaluation: Evaluation):
"%(name)s[elements___]"
elements_sequence = elements.get_sequence()
if len(elements_sequence) <= 1:
return SymbolTrue

first_item = items_sequence[0]
for item in items_sequence[1:]:
first_item = elements_sequence[0]
for item in elements_sequence[1:]:
if not first_item.sameQ(item):
return SymbolFalse
return SymbolTrue
Expand Down Expand Up @@ -840,7 +910,7 @@ class Unequal(_EqualityOperator, _SympyComparison):
sympy_name = "Ne"

@staticmethod
def _op(x):
def operator_sense(x):
return not x


Expand Down Expand Up @@ -880,14 +950,14 @@ class UnsameQ(_ComparisonOperator):

summary_text = "not literal symbolic identity"

def eval_list(self, items, evaluation):
"%(name)s[items___]"
items_sequence = items.get_sequence()
if len(items_sequence) <= 1:
def eval_list(self, elements, evaluation: Evaluation):
"%(name)s[elements___]"
elements_sequence = elements.get_sequence()
if len(elements_sequence) <= 1:
return SymbolTrue

for index, first_item in enumerate(items_sequence):
for second_item in items_sequence[index + 1 :]:
for index, first_item in enumerate(elements_sequence):
for second_item in elements_sequence[index + 1 :]:
if first_item.sameQ(second_item):
return SymbolFalse
return SymbolTrue
6 changes: 3 additions & 3 deletions mathics/core/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,16 +583,16 @@ def from_sympy(self, elements: Tuple[BaseElement, ...]) -> Expression:

# This has to come before MPMathFunction
class SympyFunction(SympyObject):
def eval(self, z, evaluation: Evaluation):
def eval(self, elements, evaluation: Evaluation):
# Note: we omit a docstring here, so as not to confuse
# function signature collector ``contribute``.

# Generic eval method that uses the class sympy_name.
# to call the corresponding sympy function. Arguments are
# converted to python and the result is converted from sympy
#
# "%(name)s[z__]"
return eval_sympy(self, z, evaluation)
# "%(name)s[elements]"
return eval_sympy(self, elements, evaluation)

def get_constant(self, precision, evaluation, have_mpmath=False):
try:
Expand Down
Loading

0 comments on commit e0755b5

Please sign in to comment.