From e4bb9144862672d04343e1f8a77dd610598a7591 Mon Sep 17 00:00:00 2001 From: David A Roberts Date: Sun, 3 Nov 2024 21:59:02 +1000 Subject: [PATCH] Miscellaneous fixes (#1153) This just fixes a few crashes I was seeing running Mathics through OEIS programs, with corresponding doctests (and a few more type error fixes) --- mathics/builtin/atomic/numbers.py | 13 ++++++++----- mathics/builtin/list/constructing.py | 9 ++++++++- mathics/builtin/list/rearrange.py | 23 +++++++++++++++-------- mathics/builtin/numbers/algebra.py | 6 +++++- mathics/builtin/numbers/calculus.py | 6 ++++-- mathics/builtin/numbers/numbertheory.py | 12 +++++++++++- mathics/core/element.py | 2 +- mathics/core/expression.py | 3 ++- 8 files changed, 54 insertions(+), 20 deletions(-) diff --git a/mathics/builtin/atomic/numbers.py b/mathics/builtin/atomic/numbers.py index e1a5246ed..6b30bf924 100644 --- a/mathics/builtin/atomic/numbers.py +++ b/mathics/builtin/atomic/numbers.py @@ -486,11 +486,14 @@ def eval_with_base(self, n, b, evaluation, nr_elements=None, pos=None): digits = [] if not py_b == 10: digits = convert_float_base(py_n, py_b, display_len - exp) - # truncate all the leading 0's - i = 0 - while digits and digits[i] == 0: - i += 1 - digits = digits[i:] + if all(d == 0 for d in digits): + digits = [0] + else: + # truncate all the leading 0's + i = 0 + while digits and digits[i] == 0: + i += 1 + digits = digits[i:] if not isinstance(n, Integer): if len(digits) > display_len: diff --git a/mathics/builtin/list/constructing.py b/mathics/builtin/list/constructing.py index 6cb53fd21..998278df9 100644 --- a/mathics/builtin/list/constructing.py +++ b/mathics/builtin/list/constructing.py @@ -184,6 +184,13 @@ class Normal(Builtin):
Brings special expressions to a normal expression from different special \ forms. + + >> Normal[Pi] + = Pi + >> Series[Exp[x], {x, 0, 5}] + = 1 + x + 1 / 2 x ^ 2 + 1 / 6 x ^ 3 + 1 / 24 x ^ 4 + 1 / 120 x ^ 5 + O[x] ^ 6 + >> Normal[%] + = 1 + x + x ^ 2 / 2 + x ^ 3 / 6 + x ^ 4 / 24 + x ^ 5 / 120 """ summary_text = "convert objects to normal expressions" @@ -191,7 +198,7 @@ class Normal(Builtin): def eval_general(self, expr: Expression, evaluation: Evaluation): "Normal[expr_]" if isinstance(expr, Atom): - return + return expr if expr.has_form("RootSum", 2): return from_sympy(expr.to_sympy().doit(roots=True)) return Expression( diff --git a/mathics/builtin/list/rearrange.py b/mathics/builtin/list/rearrange.py index 8247bcc34..a01c9a448 100644 --- a/mathics/builtin/list/rearrange.py +++ b/mathics/builtin/list/rearrange.py @@ -8,11 +8,12 @@ import functools from collections import defaultdict from itertools import chain -from typing import Callable +from typing import Callable, Optional -from mathics.core.atoms import Integer, Integer0 +from mathics.core.atoms import Integer, Integer0, Number from mathics.core.attributes import A_FLAT, A_ONE_IDENTITY, A_PROTECTED from mathics.core.builtin import Builtin, MessageException +from mathics.core.element import BaseElement from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression, structure from mathics.core.expression_predefined import MATHICS3_INFINITY @@ -655,6 +656,7 @@ class Flatten(Builtin): "Level `1` specified in `2` exceeds the levels, `3`, " "which can be flattened together in `4`." ), + "normal": "Nonatomic expression expected at position `1` in `2`.", } rules = { @@ -759,11 +761,12 @@ def insert_element(elements): return Expression(h, *insert_element(elements)) - def eval(self, expr, n, h, evaluation): + def eval(self, expr: BaseElement, n: Number, h, evaluation): "Flatten[expr_, n_, h_]" + n_int: Optional[int] if n.sameQ(MATHICS3_INFINITY): - n = -1 # a negative number indicates an unbounded level + n_int = -1 # a negative number indicates an unbounded level else: n_int = n.get_int_value() # Here we test for negative since in Mathics Flatten[] as opposed to flatten_with_respect_to_head() @@ -771,9 +774,12 @@ def eval(self, expr, n, h, evaluation): if n_int is None or n_int < 0: evaluation.message("Flatten", "flpi", n) return - n = n_int - return expr.flatten_with_respect_to_head(h, level=n) + if not isinstance(expr, Expression): + evaluation.message("Flatten", "normal", 1, expr) + return + + return expr.flatten_with_respect_to_head(h, level=n_int) class GatherBy(_GatherOperation): @@ -814,14 +820,15 @@ class GatherBy(_GatherOperation): summary_text = "gather based on values of a function applied to elements" _bin = _GatherBin - def eval(self, values, func, evaluation: Evaluation): + def eval(self, values: BaseElement, func, evaluation: Evaluation): "%(name)s[values_, func_]" if not self._check_list(values, func, evaluation): return keys = Expression(SymbolMap, func, values).evaluate(evaluation) - if len(keys.elements) != len(values.elements): + assert keys is not None + if len(keys.get_elements()) != len(values.get_elements()): return return self._gather(keys, values, _FastEquivalence()) diff --git a/mathics/builtin/numbers/algebra.py b/mathics/builtin/numbers/algebra.py index 59ec4f4c3..e9af6b01e 100644 --- a/mathics/builtin/numbers/algebra.py +++ b/mathics/builtin/numbers/algebra.py @@ -899,6 +899,8 @@ class CoefficientList(Builtin): = {{{0, 0, 0, 27}, {0, 0, -54, 0}, {0, 36, 0, 0}, {-8, 0, 0, 0}}, {{0, 0, 27, 0}, {0, -36, 0, 0}, {12, 0, 0, 0}, {0, 0, 0, 0}}, {{0, 9, 0, 0}, {-6, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}, {{1, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}} >> CoefficientList[Series[Log[1-x], {x, 0, 9}], x] = {0, -1, -1 / 2, -1 / 3, -1 / 4, -1 / 5, -1 / 6, -1 / 7, -1 / 8, -1 / 9} + >> CoefficientList[Series[2x, {x, 0, 9}], x] + = {0, 2} """ messages = { @@ -944,7 +946,9 @@ def eval(self, expr: Expression, form: Expression, evaluation: Evaluation): return ListExpression( *[ coeffs.elements[i - nmin.value] if i >= nmin.value else Integer0 - for i in range(0, nmax.value) + for i in range( + 0, min(nmax.value, nmin.value + len(coeffs.elements)) + ) ] ) diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index d638be758..1b5cfa991 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -1818,6 +1818,8 @@ class SeriesCoefficient(Builtin): = 31 / 5760 >> SeriesCoefficient[Exp[-x], {x, 0, 5}] = -1 / 120 + >> SeriesCoefficient[2x, {x, 0, 2}] + = 0 >> SeriesCoefficient[SeriesData[x, c, Table[i^2, {i, 10}], 7, 17, 3], 14/3] = 64 @@ -1842,10 +1844,10 @@ def eval(self, series: Expression, n: Rational, evaluation: Evaluation): den: Integer coeffs, nmin, nmax, den = series.elements[2:] index = n.value * den.value - nmin.value - if index < 0: - return Integer0 if index >= nmax.value - nmin.value: return SymbolIndeterminate + if index < 0 or index >= len(coeffs.elements): + return Integer0 return coeffs[index] diff --git a/mathics/builtin/numbers/numbertheory.py b/mathics/builtin/numbers/numbertheory.py index d49b1db81..1bd033c38 100644 --- a/mathics/builtin/numbers/numbertheory.py +++ b/mathics/builtin/numbers/numbertheory.py @@ -217,6 +217,8 @@ class EulerPhi(SympyFunction): 'EulerPhi' of a negative integer is same as its positive counterpart: >> EulerPhi[-11] == EulerPhi[11] = True + >> EulerPhi[0] + = 0 Large arguments are computed quickly: >> EulerPhi[40!] @@ -238,8 +240,16 @@ class EulerPhi(SympyFunction): def eval(self, n: Integer, evaluation: Evaluation): "EulerPhi[n_Integer]" + if n.is_zero: + return Integer0 return super().eval(abs(n), evaluation) + def to_sympy(self, expr, **kwargs): + try: + return super().to_sympy(expr, **kwargs) + except ValueError: # n must be a positive integer + return None + class FactorInteger(Builtin): """ @@ -441,7 +451,7 @@ class IntegerPartitions(Builtin): "IntegerPartitions[n_Integer, All]": "IntegerPartitions[n, n]", "IntegerPartitions[n_Integer, k_Integer]": "IntegerPartitions[n, {1, k}]", "IntegerPartitions[n_Integer, {k_Integer}]": "IntegerPartitions[n, {k, k}]", - "IntegerPartitions[n_Integer, kspec_, s_List] /; SubsetQ[Range[n], s]": "Select[IntegerPartitions[n, kspec], SubsetQ[s, #] &]", + "IntegerPartitions[n_Integer, kspec_, s_List] /; SubsetQ[Range[n], s] && s == Union[s]": "Select[IntegerPartitions[n, kspec], SubsetQ[s, #] &]", "IntegerPartitions[n_Integer, kspec_, All]": "IntegerPartitions[n, kspec]", "IntegerPartitions[n_Integer, kspec_, sspec_, m_]": "Take[IntegerPartitions[n, kspec, sspec], m]", "IntegerPartitions[n_Integer, {k_Integer}, s_List]": "ReverseSort@Select[Union[ReverseSort /@ Tuples[s, k]], Total[#] == n &]", diff --git a/mathics/core/element.py b/mathics/core/element.py index 708716eab..c9cf6eb0d 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -301,7 +301,7 @@ def get_head_name(self) -> str: def get_float_value(self, permit_complex=False): return None - def get_int_value(self): + def get_int_value(self) -> Optional[int]: return None def get_lookup_name(self) -> str: diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 923106211..693b14bc2 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -662,7 +662,7 @@ def sequence(element): return self._flatten_sequence(sequence, evaluation) def flatten_with_respect_to_head( - self, head, pattern_only=False, callback=None, level=100 + self, head: Symbol, pattern_only=False, callback=None, level=100 ) -> "Expression": """ Flatten elements in self which have `head` in them. @@ -1304,6 +1304,7 @@ def flatten_callback(new_elements, old): element.unevaluated = old.unevaluated if A_FLAT & attributes: + assert isinstance(new._head, Symbol) new = new.flatten_with_respect_to_head(new._head, callback=flatten_callback) if new.elements_properties is None: new._build_elements_properties()