Skip to content

Commit bbaccf5

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents c8b277c + 90d6767 commit bbaccf5

File tree

10 files changed

+360
-13
lines changed

10 files changed

+360
-13
lines changed

mathics/core/atoms.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
from typing import Any, Dict, Generic, Optional, Tuple, TypeVar, Union
88

99
import mpmath
10+
import numpy
1011
import sympy
1112
from sympy.core import numbers as sympy_numbers
1213

1314
from mathics.core.element import BoxElementMixin, ImmutableValueMixin
1415
from mathics.core.keycomparable import (
1516
BASIC_ATOM_BYTEARRAY_ELT_ORDER,
1617
BASIC_ATOM_NUMBER_ELT_ORDER,
18+
BASIC_ATOM_NUMERICARRAY_ELT_ORDER,
1719
BASIC_ATOM_STRING_ELT_ORDER,
1820
)
1921
from mathics.core.number import (
@@ -1098,6 +1100,117 @@ def is_zero(self) -> bool:
10981100
}
10991101

11001102

1103+
#
1104+
# NumericArray
1105+
#
1106+
1107+
NUMERIC_ARRAY_TYPE_MAP = {
1108+
"UnsignedInteger8": numpy.dtype("uint8"),
1109+
"UnsignedInteger16": numpy.dtype("uint16"),
1110+
"UnsignedInteger32": numpy.dtype("uint32"),
1111+
"UnsignedInteger64": numpy.dtype("uint64"),
1112+
"Integer8": numpy.dtype("int8"),
1113+
"Integer16": numpy.dtype("int16"),
1114+
"Integer32": numpy.dtype("int32"),
1115+
"Integer64": numpy.dtype("int64"),
1116+
"Real32": numpy.dtype("float32"),
1117+
"Real64": numpy.dtype("float64"),
1118+
"ComplexReal32": numpy.dtype("complex64"),
1119+
"ComplexReal64": numpy.dtype("complex128"),
1120+
}
1121+
1122+
NUMERIC_ARRAY_DTYPE_TO_NAME = {
1123+
dtype: name for name, dtype in NUMERIC_ARRAY_TYPE_MAP.items()
1124+
}
1125+
1126+
1127+
class NumericArray(Atom, ImmutableValueMixin):
1128+
"""
1129+
NumericArray provides compact storage and efficient access for machine-precision numeric arrays,
1130+
backed by NumPy arrays.
1131+
"""
1132+
1133+
class_head_name = "NumericArray"
1134+
1135+
def __init__(self, value, dtype=None):
1136+
# compute value
1137+
if not isinstance(value, numpy.ndarray):
1138+
value = numpy.asarray(value, dtype=dtype)
1139+
elif dtype is not None:
1140+
value = value.astype(dtype)
1141+
self.value = value
1142+
1143+
# check type
1144+
self._type_name = NUMERIC_ARRAY_DTYPE_TO_NAME.get(self.value.dtype, None)
1145+
if not self._type_name:
1146+
allowed = ", ".join(str(dtype) for dtype in NUMERIC_ARRAY_TYPE_MAP.values())
1147+
message = f"Argument 'value' must be one of {allowed}; is {str(self.value.dtype)}."
1148+
raise ValueError(message)
1149+
1150+
# summary and hash
1151+
shape_string = "×".join(str(dim) for dim in self.value.shape) or "0"
1152+
self._summary_string = f"{self._type_name}, {shape_string}"
1153+
self._hash = None
1154+
1155+
def __hash__(self):
1156+
if not self._hash:
1157+
self._hash = hash(("NumericArray", self.value.shape, id(self.value)))
1158+
return self._hash
1159+
1160+
def __str__(self) -> str:
1161+
return f"NumericArray[{self._summary_string}]"
1162+
1163+
def atom_to_boxes(self, f, evaluation):
1164+
return String(f"<{self._summary_string}>")
1165+
1166+
def do_copy(self) -> "NumericArray":
1167+
return NumericArray(self.value.copy())
1168+
1169+
def default_format(self, evaluation, form) -> str:
1170+
return f"NumericArray[<{self._summary_string}>]"
1171+
1172+
@property
1173+
def items(self) -> tuple:
1174+
from mathics.core.convert.python import from_python
1175+
1176+
if len(self.value.shape) == 1:
1177+
return tuple(from_python(item.item()) for item in self.value)
1178+
else:
1179+
return tuple(NumericArray(array) for array in self.value)
1180+
1181+
@property
1182+
def element_order(self) -> tuple:
1183+
return (
1184+
BASIC_ATOM_NUMERICARRAY_ELT_ORDER,
1185+
self.value.shape,
1186+
self.value.dtype,
1187+
id(self.value),
1188+
)
1189+
1190+
@property
1191+
def pattern_precedence(self) -> tuple:
1192+
return super().pattern_precedence
1193+
1194+
def sameQ(self, rhs) -> bool:
1195+
return isinstance(rhs, NumericArray) and numpy.array_equal(
1196+
self.value, rhs.value
1197+
)
1198+
1199+
def to_sympy(self, **kwargs) -> None:
1200+
return None
1201+
1202+
# TODO: this returns a list instead of np.ndarray in keeping with
1203+
# idea that to_python should return only "native" Python types.
1204+
# Keep an eye on this because there is a slight risk that code may
1205+
# naively call to_python and cause a performance issue due to
1206+
# the cost of converting to a nested list structure for a large array.
1207+
def to_python(self, *args, **kwargs) -> list:
1208+
return self.value.tolist()
1209+
1210+
def user_hash(self, update) -> None:
1211+
update(self.value.tobytes())
1212+
1213+
11011214
class String(Atom, BoxElementMixin):
11021215
value: str
11031216
class_head_name = "System`String"

mathics/core/convert/python.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@
55

66
from typing import Any
77

8-
from mathics.core.atoms import Complex, Integer, Rational, Real, String
8+
import numpy
9+
10+
from mathics.core.atoms import (
11+
ByteArray,
12+
Complex,
13+
Integer,
14+
NumericArray,
15+
Rational,
16+
Real,
17+
String,
18+
)
919
from mathics.core.number import get_type
1020
from mathics.core.symbols import (
1121
BaseElement,
@@ -14,7 +24,7 @@
1424
SymbolNull,
1525
SymbolTrue,
1626
)
17-
from mathics.core.systemsymbols import SymbolByteArray, SymbolRule
27+
from mathics.core.systemsymbols import SymbolRule
1828

1929

2030
def from_bool(arg: bool) -> BooleanType:
@@ -110,8 +120,8 @@ def from_python(arg: Any) -> BaseElement:
110120
elif isinstance(arg, list) or isinstance(arg, tuple):
111121
return to_mathics_list(*arg, elements_conversion_fn=from_python)
112122
elif isinstance(arg, bytearray) or isinstance(arg, bytes):
113-
from mathics.builtin.binary.bytearray import ByteArray
114-
115-
return Expression(SymbolByteArray, ByteArray(arg))
123+
return ByteArray(arg)
124+
elif isinstance(arg, numpy.ndarray):
125+
return NumericArray(arg)
116126
else:
117127
raise NotImplementedError

mathics/core/expression.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ def _build_elements_properties(self):
387387

388388
# and now possibly adjust self.elements_properties.elements_fully_evaluted
389389
if self.elements_properties.elements_fully_evaluated:
390-
self._elements_fully_evaluated = (
390+
self.elements_properties.elements_fully_evaluated = (
391391
element.elements_properties.elements_fully_evaluated
392392
)
393393

@@ -976,7 +976,7 @@ def is_literal(self) -> bool:
976976
# Lists. Lists definitions can't be changed right?
977977
return False
978978
# If we have a List we may do something like:
979-
# return self._elements_fully_evaluated
979+
# return self.elements_properties.elements_fully_evaluated
980980

981981
def is_uncertain_final_definitions(self, definitions) -> bool:
982982
"""

mathics/core/keycomparable.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ def __ne__(self, other) -> bool:
279279
BASIC_ATOM_STRING_ELT_ORDER = 0x01
280280
BASIC_ATOM_BYTEARRAY_ELT_ORDER = 0x02
281281
LITERAL_EXPRESSION_ELT_ORDER = 0x03
282+
BASIC_ATOM_NUMERICARRAY_ELT_ORDER = 0x04
282283

283284
BASIC_NUMERIC_EXPRESSION_ELT_ORDER = 0x12
284285
GENERAL_NUMERIC_EXPRESSION_ELT_ORDER = 0x13

mathics/core/rules.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,6 @@ def yield_match(vars, rest):
187187
# For now we have to take a pessimistic view
188188
expr = exc.value
189189
# FIXME: expr is sometimes a list - why the changing types
190-
if hasattr(expr, "_elements_fully_evaluated"):
191-
expr._elements_fully_evaluated = False
192-
expr._is_flat = False # I think this is fully updated
193-
expr._is_ordered = False
194190
if (
195191
hasattr(expression, "location")
196192
and hasattr(expr, "location")

mathics/core/util.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from platform import python_implementation
1010
from typing import Optional
1111

12+
from mathics.core.symbols import Symbol
13+
1214
IS_PYPY = python_implementation() == "PyPy"
1315

1416

@@ -113,3 +115,29 @@ def subranges(
113115
items[start : start + length],
114116
(items[:start], items[start + length :]),
115117
)
118+
119+
120+
def print_expression_tree(expr, indent="", marker=lambda expr: ""):
121+
"""
122+
Print a Mathics Expression as an indented tree.
123+
Caller may supply a marker function that computes a marker
124+
to be displayed in the tree for the given node.
125+
"""
126+
if isinstance(expr, Symbol):
127+
print(f"{indent}{marker(expr)}{expr}")
128+
elif not hasattr(expr, "elements"):
129+
print(f"{indent}{marker(expr)}{expr.get_head()} {expr}")
130+
else:
131+
print(f"{indent}{marker(expr)}{expr.head}")
132+
for elt in expr.elements:
133+
print_expression_tree(elt, indent + " ", marker=marker)
134+
135+
136+
def print_sympy_tree(expr, indent=""):
137+
"""Print a SymPy Expression as an indented tree"""
138+
if expr.args:
139+
print(f"{indent}{expr.func.__name__}")
140+
for i, arg in enumerate(expr.args):
141+
print_sympy_tree(arg, indent + " ")
142+
else:
143+
print(f"{indent}{expr.func.__name__}({str(expr)})")

test/builtin/drawing/test_plot.py

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
Unit tests from mathics.builtin.drawing.plot
44
"""
55

6-
from test.helper import check_evaluation
6+
from test.helper import check_evaluation, session
77

88
import pytest
99

10+
from mathics.core.util import print_expression_tree
11+
1012

1113
def test__listplot():
1214
"""tests for module builtin.drawing.plot._ListPlot"""
@@ -201,3 +203,125 @@ def test_plot(str_expr, msgs, str_expected, fail_msg):
201203
failure_message=fail_msg,
202204
expected_messages=msgs,
203205
)
206+
207+
208+
#
209+
# Call plotting functions and examine the structure of the output
210+
# In case of error trees are printed with an embedded >>> marker showing location of error
211+
#
212+
213+
214+
def print_expression_tree_with_marker(expr):
215+
print_expression_tree(expr, marker=lambda expr: getattr(expr, "_marker", ""))
216+
217+
218+
def check_structure(result, expected):
219+
"""Check that expected is a prefix of result at every node"""
220+
221+
def error(msg):
222+
result._marker = "RESULT >>> "
223+
expected._marker = "EXPECTED >>> "
224+
raise AssertionError(msg)
225+
226+
# do the heads match?
227+
if result.get_head() != expected.get_head():
228+
error("heads don't match")
229+
230+
# does the structure match?
231+
if hasattr(expected, "elements"):
232+
if not hasattr(result, "elements"):
233+
error("expected elements but result has none")
234+
for i, e in enumerate(expected.elements):
235+
if len(result.elements) <= i:
236+
error("result has too few elements")
237+
check_structure(result.elements[i], e)
238+
else:
239+
if str(result) != str(expected):
240+
error("leaves don't match")
241+
242+
243+
def eval_and_check_structure(str_expr, str_expected):
244+
expr = session.parse(str_expr)
245+
result = expr.evaluate(session.evaluation)
246+
expected = session.parse(str_expected)
247+
try:
248+
check_structure(result, expected)
249+
except AssertionError as oops:
250+
print(f"\nERROR: {oops} (error is marked with >>> below)")
251+
print("=== result:")
252+
print_expression_tree_with_marker(result)
253+
print("=== expected:")
254+
print_expression_tree_with_marker(expected)
255+
raise
256+
257+
258+
def test_plot3d_default():
259+
eval_and_check_structure(
260+
"""
261+
Plot3D[
262+
x+y,
263+
{x,0,1}, {y,0,1},
264+
PlotPoints->{2,2},
265+
MaxRecursion->0
266+
]
267+
""",
268+
"""
269+
Graphics3D[
270+
{
271+
Polygon[{{0.0,0.0,0.0}, {0.0,0.5,0.5}, {0.5,0.0,0.5}}],
272+
Polygon[{{}}]
273+
},
274+
AspectRatio -> 1,
275+
Axes -> True,
276+
AxesStyle -> {},
277+
Background -> Automatic,
278+
BoxRatios -> {1, 1, 0.4},
279+
ImageSize -> Automatic,
280+
LabelStyle -> {},
281+
PlotRange -> Automatic,
282+
PlotRangePadding -> Automatic,
283+
TicksStyle -> {}
284+
]
285+
""",
286+
)
287+
288+
289+
def test_plot3d_nondefault():
290+
eval_and_check_structure(
291+
"""
292+
Plot3D[
293+
x+y,
294+
{x,0,1}, {y,0,1},
295+
PlotPoints->{2,2},
296+
MaxRecursion->0
297+
AspectRatio -> 0.5,
298+
Axes -> False,
299+
AxesStyle -> {Red,Blue},
300+
Background -> Green,
301+
BoxRatios -> {10, 10, 1},
302+
ImageSize -> {200,200},
303+
LabelStyle -> Red,
304+
PlotRange -> {0,1},
305+
PlotRangePadding -> {1,2},
306+
TicksStyle -> {Purple,White}
307+
]
308+
""",
309+
"""
310+
Graphics3D[
311+
{
312+
Polygon[{{0.0,0.0,0.0}, {0.0,0.5,0.5}, {0.5,0.0,0.5}}],
313+
Polygon[{{}}]
314+
},
315+
AspectRatio -> 1, (* TODO: is not passed through apparently - or is my misunderstanding? *)
316+
Axes -> False,
317+
AxesStyle -> {RGBColor[1,0,0],RGBColor[0,0,1]},
318+
Background -> RGBColor[0,1,0],
319+
BoxRatios -> {10, 10, 1},
320+
ImageSize -> {200,200},
321+
LabelStyle -> RGBColor[1,0,0],
322+
PlotRange -> {0,1},
323+
PlotRangePadding -> {1,2},
324+
TicksStyle -> {RGBColor[0.5,0,0.5],GrayLevel[1]}
325+
]
326+
""",
327+
)

0 commit comments

Comments
 (0)