|
7 | 7 | from typing import Any, Dict, Generic, Optional, Tuple, TypeVar, Union |
8 | 8 |
|
9 | 9 | import mpmath |
| 10 | +import numpy |
10 | 11 | import sympy |
11 | 12 | from sympy.core import numbers as sympy_numbers |
12 | 13 |
|
13 | 14 | from mathics.core.element import BoxElementMixin, ImmutableValueMixin |
14 | 15 | from mathics.core.keycomparable import ( |
15 | 16 | BASIC_ATOM_BYTEARRAY_ELT_ORDER, |
16 | 17 | BASIC_ATOM_NUMBER_ELT_ORDER, |
| 18 | + BASIC_ATOM_NUMERICARRAY_ELT_ORDER, |
17 | 19 | BASIC_ATOM_STRING_ELT_ORDER, |
18 | 20 | ) |
19 | 21 | from mathics.core.number import ( |
@@ -1098,6 +1100,117 @@ def is_zero(self) -> bool: |
1098 | 1100 | } |
1099 | 1101 |
|
1100 | 1102 |
|
| 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 | + |
1101 | 1214 | class String(Atom, BoxElementMixin): |
1102 | 1215 | value: str |
1103 | 1216 | class_head_name = "System`String" |
|
0 commit comments