Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Between, correct some types... #1282

Merged
merged 5 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading