Skip to content

Commit c2af55a

Browse files
authored
Improve PredefinedExpressions (#1840)
* Add a way that `from_python()` can convert a dictionary to an `Association`. * Remove `ExpressionInfinity`. This is a duplicate of `MATHICS3_INFINITY`. * Fill in more properties for predefined constants; set Python and SymPy corresponding values * Define `ELEMENTS_FULLY_EVALUATED` for use on fully-evaluated constants.
1 parent 84315d8 commit c2af55a

11 files changed

Lines changed: 168 additions & 38 deletions

File tree

mathics/builtin/list/eol.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
PartError,
4141
PartRangeError,
4242
)
43-
from mathics.core.expression import Expression, ExpressionInfinity
43+
from mathics.core.expression import Expression
4444
from mathics.core.expression_predefined import MATHICS3_INFINITY
4545
from mathics.core.list import ListExpression
4646
from mathics.core.rules import Rule
@@ -482,7 +482,7 @@ def eval_ls_n(self, items, pattern, levelspec, n, evaluation):
482482

483483
levelspec = python_levelspec(levelspec)
484484

485-
if n is SymbolInfinity or ExpressionInfinity == n:
485+
if n is SymbolInfinity or n == MATHICS3_INFINITY:
486486
n = -1
487487
elif isinstance(n, Integer):
488488
n = n.value
@@ -1609,7 +1609,7 @@ def eval_with_n(self, items, expr, n, evaluation: Evaluation):
16091609
"Select[items_, expr_, n_]"
16101610

16111611
count_is_valid = True
1612-
if n is SymbolInfinity or ExpressionInfinity == n:
1612+
if n is SymbolInfinity or MATHICS3_INFINITY == n:
16131613
count = None
16141614
elif isinstance(n, Integer):
16151615
count = n.value

mathics/builtin/patterns/rules.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@
7575
from mathics.core.element import BaseElement
7676
from mathics.core.evaluation import Evaluation
7777
from mathics.core.exceptions import InvalidLevelspecError
78-
from mathics.core.expression import Expression, ExpressionInfinity
78+
from mathics.core.expression import Expression
79+
from mathics.core.expression_predefined import MATHICS3_INFINITY
7980
from mathics.core.list import ListExpression
8081
from mathics.core.symbols import SymbolTrue
8182
from mathics.core.systemsymbols import (
@@ -370,7 +371,7 @@ def eval(
370371
# default argument, when it is passed explicitly, e.g.
371372
# ReplaceList[expr, {}, Infinity], then Infinity
372373
# comes in as DirectedInfinity[1].
373-
if maxidx == SymbolInfinity or ExpressionInfinity == maxidx:
374+
if maxidx == SymbolInfinity or MATHICS3_INFINITY == maxidx:
374375
max_count = None
375376
else:
376377
max_count = maxidx.get_int_value()

mathics/builtin/scipy_utils/optimizers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from mathics.core.convert.function import expression_to_callable_and_args
77
from mathics.core.element import BaseElement
88
from mathics.core.evaluation import Evaluation
9-
from mathics.core.expression import Expression, ExpressionInfinity
9+
from mathics.core.expression import Expression
10+
from mathics.core.expression_predefined import MATHICS3_INFINITY
1011
from mathics.core.systemsymbols import SymbolAutomatic, SymbolFailed
1112
from mathics.eval.nevaluator import eval_N
1213

@@ -30,7 +31,7 @@ def get_tolerance_and_maxit(opts: dict, scale: float, evaluation: Evaluation):
3031
acc_goal = eval_N(acc_goal, evaluation)
3132
if acc_goal is SymbolAutomatic:
3233
acc_goal = Real(12.0)
33-
elif ExpressionInfinity == acc_goal:
34+
elif MATHICS3_INFINITY == acc_goal:
3435
acc_goal = None
3536
elif not isinstance(acc_goal, Number):
3637
acc_goal = None
@@ -40,7 +41,7 @@ def get_tolerance_and_maxit(opts: dict, scale: float, evaluation: Evaluation):
4041
prec_goal = eval_N(prec_goal, evaluation)
4142
if prec_goal is SymbolAutomatic:
4243
prec_goal = Real(12.0)
43-
elif ExpressionInfinity == prec_goal:
44+
elif MATHICS3_INFINITY == prec_goal:
4445
prec_goal = None
4546
elif not isinstance(prec_goal, Number):
4647
prec_goal = None

mathics/core/convert/python.py

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
Conversions between Python and Mathics3
44
"""
55

6-
from typing import Any
6+
import math
7+
from dataclasses import dataclass
8+
from typing import Any, Final, Optional
79

810
import numpy
911

@@ -16,6 +18,7 @@
1618
Real,
1719
String,
1820
)
21+
from mathics.core.element import ELEMENTS_FULLY_EVALUATED
1922
from mathics.core.number import get_type
2023
from mathics.core.symbols import (
2124
BaseElement,
@@ -24,7 +27,7 @@
2427
SymbolNull,
2528
SymbolTrue,
2629
)
27-
from mathics.core.systemsymbols import SymbolRule
30+
from mathics.core.systemsymbols import SymbolAssociation, SymbolRule
2831

2932

3033
def from_bool(arg: bool) -> BooleanType:
@@ -48,6 +51,46 @@ def from_complex(arg: complex) -> Complex:
4851
return Complex(real_value, imag_value)
4952

5053

54+
@dataclass(frozen=True)
55+
class ToPythonOptions:
56+
"""
57+
Stores options associated with the to_python[] builtin.
58+
59+
One initialized, this structure is immutable or frozen.
60+
"""
61+
62+
use_associations: Optional[bool] = None
63+
"""'True" if ordering should be lowercase first, 'False" if should uppercase first,
64+
and 'None' if we should use the natural alphabet ordering case."""
65+
66+
@classmethod
67+
def from_dict(cls, options: dict[str, Any]) -> "ToPythonOptions":
68+
"""Factory method that normalizes, type-checks, and builds the frozen structure
69+
from a raw dict[str, str].
70+
"""
71+
72+
# This will hold our cleaned, type-converted parameters
73+
processed_args: dict[str, Any] = {
74+
"use_associations": False,
75+
}
76+
77+
# Iterate through the user-provided options dictionary
78+
for key, option_value in options.items():
79+
80+
if not key:
81+
raise TypeError(f"ToPythonOptions: bad key: {key}")
82+
83+
# Type parsing and validation based on the target field name
84+
processed_args[key] = option_value
85+
86+
# Initialize and return the frozen dataclass using our verified arguments
87+
return cls(**processed_args)
88+
89+
90+
DEFAULT_PYTHON_OPTIONS: Final[ToPythonOptions] = ToPythonOptions.from_dict(
91+
{"use_associations": False}
92+
)
93+
5194
# Historically, from_python() was identified as a bottleneck.
5295

5396
# A large part of this was due to the inefficient monolithic
@@ -63,7 +106,6 @@ def from_complex(arg: complex) -> Complex:
63106
# of a bottleneck. So care may be warranted to make
64107
# sure from_python() isn't too slow.
65108

66-
67109
# TODO:
68110
# I think there are number of subtleties to be explained here.
69111
# In particular, the expression might been the result of evaluation
@@ -76,7 +118,7 @@ def from_complex(arg: complex) -> Complex:
76118
# symbol like underscore.
77119

78120

79-
def from_python(arg: Any) -> BaseElement:
121+
def from_python(arg: Any, options=DEFAULT_PYTHON_OPTIONS) -> BaseElement:
80122
"""Converts a Python expression into a Mathics3 expression."""
81123
from mathics.core.convert.expression import to_mathics_list
82124
from mathics.core.expression import Expression
@@ -108,20 +150,58 @@ def from_python(arg: Any) -> BaseElement:
108150
# else:
109151
# return Symbol(arg)
110152
elif isinstance(arg, dict):
153+
if options.use_associations:
154+
return association_from_dict(arg, options)
155+
# List of Rules was used before Associations came
156+
# into use in Wolfram Language. List of Rules
157+
# is still used a bit.
158+
# Note that Python dictionaries from the standpoint of
159+
# evaluation are fully evaluated
111160
entries = [
112161
Expression(
113162
SymbolRule,
114163
from_python(key),
115-
from_python(arg[key]),
164+
from_python(value),
165+
elements_properties=ELEMENTS_FULLY_EVALUATED,
116166
)
117-
for key in arg
167+
for key, value in arg.items()
118168
]
119-
return ListExpression(*entries)
169+
return ListExpression(*entries, elements_properties=ELEMENTS_FULLY_EVALUATED)
120170
elif isinstance(arg, list) or isinstance(arg, tuple):
121171
return to_mathics_list(*arg, elements_conversion_fn=from_python)
122172
elif isinstance(arg, bytearray) or isinstance(arg, bytes):
123173
return ByteArray(arg)
124174
elif isinstance(arg, numpy.ndarray):
125175
return NumericArray(arg)
176+
elif arg == math.inf:
177+
from mathics.core.expression_predefined import MATHICS3_INFINITY
178+
179+
return MATHICS3_INFINITY
180+
elif arg == -math.inf:
181+
from mathics.core.expression_predefined import MATHICS3_NEG_INFINITY
182+
183+
return MATHICS3_NEG_INFINITY
126184
else:
127185
raise NotImplementedError
186+
187+
188+
def association_from_dict(arg: dict, options: ToPythonOptions) -> BaseElement:
189+
"""
190+
Convert a Python dictionary into a Mathics3 Association.
191+
"""
192+
from mathics.core.expression import Expression
193+
194+
entries = [
195+
Expression(
196+
SymbolRule,
197+
from_python(key, options),
198+
from_python(value, options),
199+
)
200+
for key, value in arg.items()
201+
]
202+
result = Expression(
203+
SymbolAssociation,
204+
*entries,
205+
elements_properties=ELEMENTS_FULLY_EVALUATED,
206+
)
207+
return result

mathics/core/element.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from abc import ABC
99
from dataclasses import dataclass
10-
from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Tuple, Union
10+
from typing import TYPE_CHECKING, Any, Dict, Final, Optional, Sequence, Tuple, Union
1111

1212
from mathics.core.attributes import A_NO_ATTRIBUTES
1313
from mathics.core.keycomparable import KeyComparable
@@ -81,6 +81,11 @@ class ElementsProperties:
8181
is_uniform: bool = False
8282

8383

84+
ELEMENTS_FULLY_EVALUATED: Final[ElementsProperties] = ElementsProperties(
85+
elements_fully_evaluated=True
86+
)
87+
88+
8489
class ImmutableValueMixin:
8590
@property
8691
def is_literal(self) -> bool:

mathics/core/expression.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import sympy
2222
from mathics_scanner.location import SourceRange, SourceRange2
2323

24-
from mathics.core.atoms import Integer1, String
24+
from mathics.core.atoms import String
2525
from mathics.core.attributes import (
2626
A_FLAT,
2727
A_HOLD_ALL,
@@ -2025,6 +2025,3 @@ def convert_expression_elements(
20252025

20262026
def string_list(head, elements, evaluation):
20272027
return atom_list_constructor(evaluation, head, "String")(elements)
2028-
2029-
2030-
ExpressionInfinity = Expression(SymbolDirectedInfinity, Integer1)
Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
"""
2+
Module for defining constant compound Mathics3 expressions.
3+
"""
4+
5+
import math
6+
from typing import Final
7+
8+
from sympy import I, S
9+
110
from mathics.core.atoms import (
211
MATHICS3_COMPLEX_I,
312
MATHICS3_COMPLEX_I_NEG,
@@ -10,19 +19,35 @@
1019

1120

1221
class PredefinedExpression(Expression):
22+
"""
23+
(Compound) constant Mathics3 Expressions.
24+
"""
25+
1326
def __init__(
1427
self,
1528
head: BaseElement,
1629
*elements: BaseElement,
30+
sympy=None,
31+
value=None,
1732
):
1833
elements_properties = ElementsProperties(True, True, True)
1934
super().__init__(head, *elements, elements_properties=elements_properties)
35+
if sympy is not None:
36+
self._sympy = sympy
37+
if value is not None:
38+
self.value = value
2039

2140

22-
MATHICS3_COMPLEX_INFINITY = PredefinedExpression(SymbolDirectedInfinity)
23-
MATHICS3_INFINITY = PredefinedExpression(SymbolDirectedInfinity, Integer1)
24-
MATHICS3_NEG_INFINITY = PredefinedExpression(SymbolDirectedInfinity, IntegerM1)
25-
MATHICS3_I_INFINITY = PredefinedExpression(SymbolDirectedInfinity, MATHICS3_COMPLEX_I)
26-
MATHICS3_I_NEG_INFINITY = PredefinedExpression(
27-
SymbolDirectedInfinity, MATHICS3_COMPLEX_I_NEG
41+
MATHICS3_COMPLEX_INFINITY: Final = PredefinedExpression(SymbolDirectedInfinity)
42+
MATHICS3_INFINITY: Final = PredefinedExpression(
43+
SymbolDirectedInfinity, Integer1, value=math.inf, sympy=S.Infinity
44+
)
45+
MATHICS3_NEG_INFINITY: Final = PredefinedExpression(
46+
SymbolDirectedInfinity, IntegerM1, value=-math.inf, sympy=S.NegativeInfinity
47+
)
48+
MATHICS3_I_INFINITY: Final = PredefinedExpression(
49+
SymbolDirectedInfinity, MATHICS3_COMPLEX_I, sympy=S.ComplexInfinity
50+
)
51+
MATHICS3_I_NEG_INFINITY: Final = PredefinedExpression(
52+
SymbolDirectedInfinity, MATHICS3_COMPLEX_I_NEG, sympy=-I
2853
)

mathics/core/list.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(
4141
self.pattern_sequence = False
4242
self._head = SymbolList
4343
self._sympy = None
44+
self.value = None
4445

4546
# For debugging:
4647

mathics/core/rules.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ def pattern_precedence(self) -> tuple:
232232
return tuple((self.system, self.pattern.pattern_precedence))
233233

234234

235-
# FIXME: the class name would be better called RewriteRule.
235+
# FIXME: Given what is stated in the docstring below,
236+
# the class name would be better called RewriteRule, instead of the
237+
# more generic term Rule.
236238
class Rule(BaseRule):
237239
"""There are two kinds of Rules. This kind of is a rewrite rule
238240
and transforms an Expression into another Expression based on the

mathics/core/symbols.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22
# -*- coding: utf-8 -*-
33

44
import sys
5-
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Optional, Sequence, Union
5+
from typing import (
6+
TYPE_CHECKING,
7+
Any,
8+
Dict,
9+
FrozenSet,
10+
List,
11+
Optional,
12+
Sequence,
13+
Union,
14+
cast,
15+
)
616

717
from mathics.core.element import (
818
BaseElement,
@@ -775,13 +785,20 @@ def sympy_name(mathics_symbol: Symbol):
775785
# show that this does not change the output in any way.
776786
#
777787
# That said, for now we will proceed very conservatively and
778-
# cautiously. However we may decide in the future to
788+
# cautiously. However, we may decide in the future to
779789
# more of the below and in systemsymbols
780790
# PredefineSymbol.
781791

782-
SymbolFalse = BooleanType("System`False", value=False)
792+
# Note getting all checkers to agree is a nightmare.
793+
# Without that cast, some checkers complain in the *use* of SymbolFalse, that
794+
# there is a type mismatch because the Boolean __new__ seems to produce
795+
# BooleanType | Symbol | SymbolConstant rather that Boolean type.
796+
# But then without the ignore "# type", other checkers complain
797+
# that the cast is unnecessary!
798+
799+
SymbolFalse = cast(BooleanType, BooleanType("System`False", value=False)) # type: ignore
783800
SymbolList = SymbolConstant("System`List", value=list)
784-
SymbolTrue = BooleanType("System`True", value=True)
801+
SymbolTrue = cast(BooleanType, BooleanType("System`True", value=True)) # type: ignore
785802

786803
SymbolAbs = Symbol("Abs")
787804
SymbolDivide = Symbol("Divide")

0 commit comments

Comments
 (0)