From b91f9f4249e35f12e7945e2b7df6d092f2380db9 Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 21 Sep 2026 19:43:53 -0400 Subject: [PATCH 1/2] Move round_to_float() method from NumericOperators to Symbol Remove weird "permit_complex" parameter --- mathics/core/atoms/numerics.py | 9 ++++++++- mathics/core/expression.py | 10 +++++----- mathics/core/symbols.py | 34 +++++++++++++++------------------- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/mathics/core/atoms/numerics.py b/mathics/core/atoms/numerics.py index 1cc323ddf..803ea10e4 100644 --- a/mathics/core/atoms/numerics.py +++ b/mathics/core/atoms/numerics.py @@ -35,7 +35,14 @@ min_prec, prec, ) -from mathics.core.symbols import Atom, NumericOperators, Symbol, SymbolNull, symbol_set +from mathics.core.symbols import ( + Atom, + NumericOperators, + Symbol, + SymbolN, + SymbolNull, + symbol_set, +) from mathics.core.systemsymbols import ( SymbolFullForm, SymbolI, diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 219c0738b..4f1671b01 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -996,11 +996,11 @@ def element_order(self) -> tuple: if element.has_form(SymbolPower, 2): var = element.get_element(0).get_name() expr = element.get_element(1) - assert isinstance(expr, (Expression, NumericOperators)) - exp = expr.round_to_float() - if var and exp is not None: - var = wma_str_sort_key(var) - exps[var] = exps.get(var, 0) + exp + if hasattr(expr, "round_to_float"): + exp = expr.round_to_float() + if var and exp is not None: + var = wma_str_sort_key(var) + exps[var] = exps.get(var, 0) + exp elif name: name = wma_str_sort_key(name) exps[name] = exps.get(name, 0) + 1 diff --git a/mathics/core/symbols.py b/mathics/core/symbols.py index 6aebc95be..9c160836e 100644 --- a/mathics/core/symbols.py +++ b/mathics/core/symbols.py @@ -85,25 +85,6 @@ def __floordiv__(self, other) -> BaseElement: def __pow__(self, other) -> BaseElement: return self.create_expression(SymbolPower, self, other) - # FIXME: The name "round_to_float" is misleading when - # permit_complex is True. - def round_to_float( - self, evaluation=None, permit_complex=False - ) -> Optional[Union[complex, float]]: - """ - Round to a Python float. Return None if rounding is not possible. - This can happen if self or evaluation is NaN. - """ - value = ( - self - if evaluation is None - else self.create_expression(SymbolN, self).evaluate(evaluation) - ) - if hasattr(value, "round") and hasattr(value, "get_float_value"): - value = value.round() - return value.get_float_value(permit_complex=permit_complex) - return None - def strip_context(name) -> str: """strip context from a symbol name""" @@ -592,6 +573,21 @@ def replace_vars( # assert all(fully_qualified_symbol_name(v) for v in vars) return vars.get(self.name, self) + def round_to_float(self, evaluation=None) -> Optional[float]: + """ + Round to a Python float. Return None if rounding is not possible. + This can happen if self or evaluation is NaN. + """ + value = ( + self + if evaluation is None + else self.create_expression(SymbolN, self).evaluate(evaluation) + ) + if hasattr(value, "round") and hasattr(value, "get_float_value"): + value = value.round() + return value.get_float_value() + return None + def sameQ(self, rhs: Any) -> bool: """Mathics3 SameQ""" return self is rhs From bfe00fd0b7b6da1f6d4e39fecb949e18771a5725 Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 21 Sep 2026 20:47:15 -0400 Subject: [PATCH 2/2] Start to deal with is_zero... Not successful here, but we at least we can provide a top-level function, and start to narrow its use. Also tag a constant DirectedInfity map. --- mathics/builtin/arithmetic.py | 9 +++++---- mathics/core/atoms/numerics.py | 16 ++++++++++++++++ mathics/eval/numbers/calculus/series.py | 13 +++++++------ mathics/eval/numeric.py | 12 ++++++------ 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/mathics/builtin/arithmetic.py b/mathics/builtin/arithmetic.py index 155964b0f..1ee36fc26 100644 --- a/mathics/builtin/arithmetic.py +++ b/mathics/builtin/arithmetic.py @@ -7,7 +7,7 @@ """ import sys -from typing import Optional +from typing import Any, Final, Optional import sympy @@ -82,7 +82,7 @@ sort_order = "mathics.builtin.mathematical-functions" -map_direction_infinity = { +MAP_DIRECTION_INFINITY: Final[dict[Any, PredefinedExpression]] = { Integer1: MATHICS3_INFINITY, IntegerM1: MATHICS3_NEG_INFINITY, MATHICS3_COMPLEX_I: MATHICS3_I_INFINITY, @@ -435,7 +435,8 @@ def eval_complex_infinity(self, evaluation: Evaluation): def eval_directed_infinity(self, direction, evaluation: Evaluation): """DirectedInfinity[direction_]""" - result = map_direction_infinity.get(direction, None) + + result = MAP_DIRECTION_INFINITY.get(direction, None) if result is not None: return result @@ -447,7 +448,7 @@ def eval_directed_infinity(self, direction, evaluation: Evaluation): if direction is None: return None - result = map_direction_infinity.get(direction, None) + result = MAP_DIRECTION_INFINITY.get(direction, None) if result is not None: return result if direction.is_zero: diff --git a/mathics/core/atoms/numerics.py b/mathics/core/atoms/numerics.py index 803ea10e4..9fb8edd5d 100644 --- a/mathics/core/atoms/numerics.py +++ b/mathics/core/atoms/numerics.py @@ -133,6 +133,14 @@ def is_inexact(self) -> bool: """ return False + @property + def is_zero(self) -> bool: + """ + If element is some sort of numeric type, Return True is "element" is zero, and False otherwise. + Subclass methods should override this. The default is False though. + """ + return False + @property def is_literal(self) -> bool: """Number can't change and has a Python representation, @@ -1219,3 +1227,11 @@ def is_integer_rational_or_real(expr) -> bool: Return True if expr is either an Integer, Rational, or Real. """ return isinstance(expr, (Integer, Rational, Real)) + + +def is_zero(element) -> Optional[bool]: + """ + If element is some sort of numeric type, Return True is "element" is zero, and False otherwise. + If it is not a numeric type, return None. + """ + return element.is_zero if hasattr(element, "is_zero") else None diff --git a/mathics/eval/numbers/calculus/series.py b/mathics/eval/numbers/calculus/series.py index cf67ce349..8acd0cff1 100644 --- a/mathics/eval/numbers/calculus/series.py +++ b/mathics/eval/numbers/calculus/series.py @@ -3,6 +3,7 @@ Implementation of Series handling functions. """ from mathics.core.atoms import Integer, Integer0, Rational +from mathics.core.atoms.numerics import is_zero from mathics.core.convert.expression import to_mathics_list from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation @@ -28,7 +29,7 @@ def same_monomial(expr, x, x0): """ Checks if expr == (x-x0) """ - if x0.is_zero and expr.sameQ(x): + if is_zero(x0) and expr.sameQ(x): return True if expr.get_head() is not SymbolPlus: return False @@ -175,7 +176,7 @@ def series_plus_series(series1, series2): for k, coeff in enumerate(data2): p = k * int(den2 / den) + offset2 if p < len_newdata: - if data[p].is_zero: + if is_zero(data[p]): data[p] = coeff else: data[p] = Expression(SymbolPlus, data[p], coeff) @@ -208,7 +209,7 @@ def series_times_series(series1, series2): pos = k1 * offset1 + k2 * offset2 if pos >= len_newdata: continue - if data[pos].is_zero: + if is_zero(data[pos]): data[pos] = Expression(SymbolTimes, c1, c2) elif data[pos].get_head() is SymbolPlus: data[pos] = Expression( @@ -254,7 +255,7 @@ def reduce_series_trailing_zeros(series): if len(data) == 0: return series i = 0 - while i < len(data) and data[i].is_zero: + while i < len(data) and is_zero(data[i]): i = i + 1 nmin = nmin + i data = data[i:] @@ -285,7 +286,7 @@ def reduce_dataseries(series, factor): while notdone: if (den % factor == 0) and (nmin % factor == 0) and (nmax % factor == 0): if all( - q.is_zero for q in data[1 + factor :: 2] for r in range(factor - 1) + is_zero(q) for q in data[1 + factor :: 2] for r in range(factor - 1) ): data = data[0::factor] nmin, nmax, den = ( @@ -317,7 +318,7 @@ def reduce_series_plus(series, terms, x, x0): # Loop over terms for term in terms: - if term.is_zero: + if is_zero(term): continue if isinstance(term, Atom): other_terms.append(term) diff --git a/mathics/eval/numeric.py b/mathics/eval/numeric.py index 4e398ce5d..0fa15c979 100644 --- a/mathics/eval/numeric.py +++ b/mathics/eval/numeric.py @@ -28,7 +28,7 @@ RationalOneHalf, Real, ) -from mathics.core.atoms.numerics import is_inexact +from mathics.core.atoms.numerics import is_inexact, is_zero from mathics.core.convert.mpmath import from_mpmath from mathics.core.convert.sympy import from_sympy from mathics.core.element import BaseElement @@ -94,7 +94,7 @@ def eval_Abs(expr: BaseElement) -> Optional[BaseElement]: return eval_Abs_number(expr) if expr.has_form(SymbolPower, 2): base, exp = expr.elements - if exp.is_zero: + if is_zero(exp): return Integer1 if test_arithmetic_expr(expr): abs_base = eval_Abs(base) @@ -180,7 +180,7 @@ def eval_RealSign(expr: BaseElement) -> Optional[Integer]: If the argument is a real algebraic expression, return the sign of the expression. """ - if expr.is_zero: + if is_zero(expr): return Integer0 if isinstance(expr, (Integer, Rational, Real)): return Integer1 if expr.value > 0 else IntegerM1 @@ -188,7 +188,7 @@ def eval_RealSign(expr: BaseElement) -> Optional[Integer]: return Integer1 if expr.has_form(SymbolAbs, 1): arg = expr.elements[0] - if arg.is_zero: + if is_zero(arg): return Integer0 if isinstance(arg, Number): return Integer1 @@ -196,7 +196,7 @@ def eval_RealSign(expr: BaseElement) -> Optional[Integer]: arg_inexact = to_inexact_value(arg) if arg_inexact is None: return None - if arg_inexact.is_zero: + if is_zero(arg_inexact): return Integer0 if isinstance(arg_inexact, Number): return Integer1 @@ -330,7 +330,7 @@ def eval_complex_sign(n: BaseElement) -> Optional[BaseElement]: # SymPy conversion failed; fall back. return None - if abs_expr.is_zero: + if is_zero(abs_expr): return abs_expr if abs_expr is Integer1: return n