Skip to content

Commit 0798348

Browse files
bdlucas1mmaterarocky
authored
Numericarray atom (#1512)
Initial feature for #1511: implement NumericArray atom. This is sufficient to support the Plot3D/Graphics3D use case as those will only require NumericArrays generated and consumed in Python. --------- Co-authored-by: Juan Mauricio Matera <[email protected]> Co-authored-by: R. Bernstein <[email protected]>
1 parent c6e5d28 commit 0798348

File tree

5 files changed

+183
-2
lines changed

5 files changed

+183
-2
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
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 Complex, Integer, NumericArray, Rational, Real, String
911
from mathics.core.number import get_type
1012
from mathics.core.symbols import (
1113
BaseElement,
@@ -113,5 +115,7 @@ def from_python(arg: Any) -> BaseElement:
113115
from mathics.builtin.binary.bytearray import ByteArray
114116

115117
return Expression(SymbolByteArray, ByteArray(arg))
118+
elif isinstance(arg, numpy.ndarray):
119+
return NumericArray(arg)
116120
else:
117121
raise NotImplementedError

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

test/builtin/test_binary.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,3 +421,38 @@ def test_ByteOrdering(str_expr, str_expected, fail_msg):
421421
hold_expected=True,
422422
failure_message=fail_msg,
423423
)
424+
425+
426+
@pytest.mark.skip(reason="NumericArray[] builtin not written yet.")
427+
@pytest.mark.parametrize(
428+
("str_expr", "str_expected"),
429+
[
430+
("NumericArray[{{1,2},{3,4}}]", "<Integer64, 2×2>"),
431+
("ToString[NumericArray[{{1,2},{3,4}}]]", "<Integer64, 2×2>"),
432+
("Head[NumericArray[{1,2}]]", "NumericArray"),
433+
("AtomQ[NumericArray[{1,2}]]", "True"),
434+
("First[NumericArray[{1,2,3}]]", "1"),
435+
("First[NumericArray[{{1,2}, {3,4}}]]", "<Integer64, 2>"),
436+
("Last[NumericArray[{1,2,3}]]", "3"),
437+
("Last[NumericArray[{{1,2}, {3,4}}]]", "<Integer64, 2>"),
438+
("Normal[NumericArray[{{1,2}, {3,4}}]]", "{{1, 2}, {3, 4}}"),
439+
],
440+
)
441+
def test_basics(str_expr, str_expected):
442+
check_evaluation(str_expr, str_expected, hold_expected=True)
443+
444+
445+
@pytest.mark.skip(reason="NumericArray[] builtin not written yet.")
446+
def test_type_conversion():
447+
# Move below imports to the top when we've implementated NumericArray[]
448+
from test.helper import evaluate
449+
450+
import numpy as np
451+
452+
from mathics.core.atoms import NumericArray
453+
454+
expr = evaluate("NumericArray[{1,2}]")
455+
assert isinstance(expr, NumericArray)
456+
assert expr.value.dtype == np.int64
457+
expr = evaluate('NumericArray[{1,2}, "ComplexReal32"]')
458+
assert expr.value.dtype == np.complex64

test/core/test_atoms.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33

4-
import sys
4+
import numpy as np
55

66
import mathics.core.atoms as atoms
77
import mathics.core.systemsymbols as system_symbols
@@ -12,11 +12,13 @@
1212
Integer2,
1313
MachineReal,
1414
MachineReal0,
15+
NumericArray,
1516
Rational,
1617
RationalOneHalf,
1718
Real,
1819
String,
1920
)
21+
from mathics.core.convert.python import from_python
2022
from mathics.core.definitions import Definitions
2123
from mathics.core.evaluation import Evaluation
2224
from mathics.core.expression import Expression
@@ -238,3 +240,29 @@ def test_mixed_object_canonicalization():
238240
Complex(Rational(1, 0), Integer(0)), # 3
239241
)
240242
# fmt: on
243+
244+
245+
#
246+
# NumericArray tests
247+
#
248+
249+
250+
def test_numericarray_atom_preserves_array_reference():
251+
array = np.array([1, 2, 3], dtype=np.int64)
252+
atom = NumericArray(array)
253+
assert atom.value is array, "NumericArray.value should be a NumPy array"
254+
255+
256+
def test_numericarray_atom_preserves_equality():
257+
array = np.array([1, 2, 3], dtype=np.int64)
258+
atom = NumericArray(array, dtype=np.float64)
259+
np.testing.assert_array_equal(atom.value, array)
260+
261+
262+
def test_numericarray_expression_from_python_array():
263+
array = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32)
264+
atom = from_python(array)
265+
assert isinstance(
266+
atom, NumericArray
267+
), "from_python() conversion of a NumPy Array should yield a NumericArray"
268+
assert atom.value is array

0 commit comments

Comments
 (0)