Skip to content

Commit deccf9d

Browse files
committed
Squashed commit of the following:
commit 85b627d Author: Bruce Lucas <[email protected]> Date: Sun Nov 9 10:59:08 2025 -0500 More tests commit 9ab56ff Author: Bruce Lucas <[email protected]> Date: Sun Nov 9 10:59:00 2025 -0500 Fix hash, element_order, items commit 21e7b31 Author: Bruce Lucas <[email protected]> Date: Sun Nov 9 10:58:12 2025 -0500 Generalize using Atom.items commit 77b8555 Author: Bruce Lucas <[email protected]> Date: Sun Nov 9 08:19:36 2025 -0500 Fix tests commit 088f68a Author: Bruce Lucas <[email protected]> Date: Thu Nov 6 13:34:06 2025 -0500 WIP commit c2b0ace Author: Bruce Lucas <[email protected]> Date: Thu Nov 6 13:29:23 2025 -0500 WIP commit 9cb4da1 Author: Bruce Lucas <[email protected]> Date: Thu Nov 6 13:22:54 2025 -0500 WIP commit 2814dd5 Author: Bruce Lucas <[email protected]> Date: Thu Nov 6 08:37:29 2025 -0500 WIP commit 36ee614 Author: Bruce Lucas <[email protected]> Date: Thu Nov 6 07:31:12 2025 -0500 WIP commit 607a53a Author: Bruce Lucas <[email protected]> Date: Wed Nov 5 22:31:59 2025 -0500 Update numericarray.py commit 6b65a80 Author: Bruce Lucas <[email protected]> Date: Wed Nov 5 17:47:39 2025 -0500 WIP commit c8b277c Merge: 455e428 c6e5d28 Author: Bruce Lucas <[email protected]> Date: Sun Nov 9 08:40:54 2025 -0500 Merge remote-tracking branch 'upstream/master' commit 455e428 Author: Bruce Lucas <[email protected]> Date: Fri Nov 7 11:21:49 2025 -0500 Update issue templates
1 parent c6e5d28 commit deccf9d

File tree

11 files changed

+290
-120
lines changed

11 files changed

+290
-120
lines changed

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 0 additions & 87 deletions
This file was deleted.

.github/ISSUE_TEMPLATE/feature_request.md

Lines changed: 0 additions & 21 deletions
This file was deleted.

SYMBOLS_MANIFEST.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,7 @@ System`NumberQ
845845
System`NumberString
846846
System`Numerator
847847
System`NumericFunction
848+
System`NumericArray
848849
System`NumericQ
849850
System`O
850851
System`Octahedron

mathics/builtin/list/constructing.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
from typing import Optional, Tuple
1414

1515
from mathics.builtin.box.layout import RowBox
16-
from mathics.core.atoms import ByteArray, Integer, Integer1, is_integer_rational_or_real
16+
from mathics.core.atoms import ByteArray, Integer, Integer1, is_integer_rational_or_real, NumericArray
1717
from mathics.core.attributes import A_HOLD_FIRST, A_LISTABLE, A_LOCKED, A_PROTECTED
1818
from mathics.core.builtin import BasePattern, Builtin, IterationFunction
1919
from mathics.core.convert.expression import to_expression
20+
from mathics.core.convert.python import from_python
2021
from mathics.core.convert.sympy import from_sympy
2122
from mathics.core.element import ElementsProperties
2223
from mathics.core.evaluation import Evaluation
@@ -198,8 +199,14 @@ class Normal(Builtin):
198199
def eval_general(self, expr: Expression, evaluation: Evaluation):
199200
"Normal[expr_]"
200201
if isinstance(expr, Atom):
201-
if isinstance(expr, ByteArray):
202-
return ListExpression(*expr.items)
202+
if hasattr(expr, "items"):
203+
def normal(items):
204+
return ListExpression(*(
205+
normal(item.items) if isinstance(item, Atom) and hasattr(item, "items")
206+
else item
207+
for item in items
208+
))
209+
return normal(expr.items)
203210
return expr
204211
if expr.has_form("RootSum", 2):
205212
return from_sympy(expr.to_sympy().doit(roots=True))

mathics/builtin/list/eol.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -698,25 +698,24 @@ def eval(self, expr, evaluation: Evaluation, expression: Expression):
698698
if not hasattr(expr, "items"):
699699
evaluation.message("First", "normal", Integer1, expression)
700700
return
701-
expr_len = len(expr.items)
701+
parts = expr.items
702702
else:
703-
expr_len = len(expr.elements)
703+
parts = expr.elements
704+
705+
expr_len = len(parts)
704706
if expr_len == 0:
705707
evaluation.message("First", "nofirst", expr)
706708
return
707709

708-
if isinstance(expr, ByteArray):
709-
return expr.items[0]
710-
711-
if expr_len > 2 and expr.head is SymbolSequence:
710+
if expr_len > 2 and expr.get_head() is SymbolSequence:
712711
evaluation.message(
713712
"First", "argt", SymbolFirst, Integer(expr_len), Integer1, Integer2
714713
)
715714
return
716715

717-
first_elem = expr.elements[0]
716+
first_elem = parts[0]
718717

719-
if expr.head == SymbolSequence or (
718+
if expr.get_head() == SymbolSequence or (
720719
not isinstance(expr, ListExpression)
721720
and len == 2
722721
and isinstance(first_elem, Atom)

mathics/builtin/numericarray.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# -*- coding: utf-8 -*-
2+
"""Rules for working with NumericArray atoms."""
3+
4+
from typing import Optional, Tuple
5+
6+
try: # pragma: no cover - numpy is optional at runtime
7+
import numpy
8+
except ImportError: # pragma: no cover - handled via requires attribute
9+
numpy = None
10+
11+
from mathics.core.atoms import NumericArray, NUMERIC_ARRAY_TYPE_MAP, String
12+
from mathics.core.builtin import Builtin
13+
from mathics.core.convert.python import from_python
14+
from mathics.core.symbols import Symbol, strip_context
15+
from mathics.core.systemsymbols import SymbolAutomatic, SymbolFailed, SymbolNumericArray
16+
17+
18+
# class name modeled on Complex_ to avoid collision with NumericArray atom
19+
class NumericArray_(Builtin):
20+
21+
summary_text = "construct NumericArray"
22+
name = "NumericArray"
23+
rules = {
24+
"NumericArray[list_List]": "NumericArray[list, Automatic]"
25+
}
26+
messages = {
27+
"type": "The type specification `1` is not supported in NumericArray.",
28+
}
29+
30+
# rule to convert NumericArray[...nested list...] expression to NumericArray atom
31+
def eval_list(self, data, typespec, evaluation):
32+
"System`NumericArray[data_List, typespec_]"
33+
34+
# get a string key from the typespec
35+
if isinstance(typespec, Symbol):
36+
key = strip_context(typespec.get_name())
37+
elif isinstance(typespec, String):
38+
key = typespec.value
39+
else:
40+
evaluation.message("NumericArray", "type", typespec)
41+
return SymbolFailed
42+
43+
# compute numpy dtype from key
44+
if key == "Automatic":
45+
dtype = None
46+
else:
47+
dtype = NUMERIC_ARRAY_TYPE_MAP.get(key, None)
48+
if not dtype:
49+
evaluation.message("NumericArray", "type", typespec)
50+
return SymbolFailed
51+
52+
# compute array from data and dtype and wrap it in a NumericArray atom
53+
python_value = data.to_python()
54+
array = numpy.array(python_value, dtype=dtype)
55+
atom = NumericArray(array, dtype)
56+
57+
return atom
58+
59+
# this doesn't work
60+
# instead see Normal builtin in mathics/builtin/list/constructing.py
61+
#def eval_normal(self, array, evaluation):
62+
# "System`Normal[array_NumericArray]"
63+
# return from_python(array.value.tolist())

mathics/core/atoms.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@
1010
import sympy
1111
from sympy.core import numbers as sympy_numbers
1212

13+
try: # pragma: no cover - optional dependency handled at runtime
14+
import numpy
15+
except ImportError: # pragma: no cover - numpy is optional at import time
16+
numpy = None
17+
1318
from mathics.core.element import BoxElementMixin, ImmutableValueMixin
1419
from mathics.core.keycomparable import (
1520
BASIC_ATOM_BYTEARRAY_ELT_ORDER,
1621
BASIC_ATOM_NUMBER_ELT_ORDER,
22+
BASIC_ATOM_NUMERICARRAY_ELT_ORDER,
1723
BASIC_ATOM_STRING_ELT_ORDER,
1824
)
1925
from mathics.core.number import (
@@ -1098,6 +1104,125 @@ def is_zero(self) -> bool:
10981104
}
10991105

11001106

1107+
#
1108+
# NumericArray
1109+
#
1110+
1111+
if numpy is not None:
1112+
NUMERIC_ARRAY_TYPE_MAP = {
1113+
"UnsignedInteger8": numpy.dtype("uint8"),
1114+
"UnsignedInteger16": numpy.dtype("uint16"),
1115+
"UnsignedInteger32": numpy.dtype("uint32"),
1116+
"UnsignedInteger64": numpy.dtype("uint64"),
1117+
"Integer8": numpy.dtype("int8"),
1118+
"Integer16": numpy.dtype("int16"),
1119+
"Integer32": numpy.dtype("int32"),
1120+
"Integer64": numpy.dtype("int64"),
1121+
"Real32": numpy.dtype("float32"),
1122+
"Real64": numpy.dtype("float64"),
1123+
"ComplexReal32": numpy.dtype("complex64"),
1124+
"ComplexReal64": numpy.dtype("complex128"),
1125+
}
1126+
NUMERIC_ARRAY_DTYPE_TO_NAME = {
1127+
dtype: name for name, dtype in NUMERIC_ARRAY_TYPE_MAP.items()
1128+
}
1129+
else: # pragma: no cover - executed only when numpy is absent
1130+
NUMERIC_ARRAY_TYPE_MAP = {}
1131+
NUMERIC_ARRAY_DTYPE_TO_NAME = {}
1132+
1133+
1134+
# TODO: would it be useful to follow the example of Complex and parameterize by type?
1135+
# would that be array.dtype or the MMA type from the map above?
1136+
class NumericArray(Atom, ImmutableValueMixin):
1137+
"""
1138+
NumericArray provides compact storage and efficient access for machine-precision numeric arrays,
1139+
backed by NumPy arrays.
1140+
"""
1141+
1142+
class_head_name = "NumericArray"
1143+
1144+
def __init__(self, value, dtype=None):
1145+
if numpy is None:
1146+
raise ImportError("numpy is required for NumericArray")
1147+
1148+
# compute value
1149+
if not isinstance(value, numpy.ndarray):
1150+
value = numpy.asarray(value, dtype=dtype)
1151+
elif dtype is not None:
1152+
value = value.astype(dtype)
1153+
self.value = value
1154+
1155+
# check type
1156+
self._type_name = NUMERIC_ARRAY_DTYPE_TO_NAME.get(self.value.dtype, None)
1157+
if not self._type_name:
1158+
allowed = ", ".join(str(dtype) for dtype in NUMERIC_ARRAY_TYPE_MAP.values())
1159+
message = f"Argument 'value' must be one of {allowed}; is {str(self.value.dtype)}."
1160+
raise ValueError(message)
1161+
1162+
# summary and hash
1163+
shape_string = "×".join(str(dim) for dim in self.value.shape) or "0"
1164+
self._summary_string = f"{self._type_name}, {shape_string}"
1165+
self._hash = None
1166+
1167+
# TODO: this is potentially expensive - what if we left it unimplemented? is hashing a numpy array reasonable?
1168+
# TODO: to make it less expensive only look at first 100 bytes - ok? needed?
1169+
def __hash__(self):
1170+
if not self._hash:
1171+
self._hash = hash(("NumericArray", self.value.shape, self.value.tobytes()[:100]))
1172+
return self._hash
1173+
1174+
def __str__(self) -> str:
1175+
return f"NumericArray[{self._summary_string}]"
1176+
1177+
def atom_to_boxes(self, f, evaluation):
1178+
return String(f"<{self._summary_string}>")
1179+
1180+
def do_copy(self) -> "NumericArray":
1181+
return NumericArray(self.value.copy())
1182+
1183+
def default_format(self, evaluation, form) -> str:
1184+
return f"NumericArray[<{self._summary_string}>]"
1185+
1186+
@property
1187+
def items(self) -> Tuple:
1188+
from mathics.core.convert.python import from_python
1189+
if len(self.value.shape) == 1:
1190+
return tuple(from_python(item.item()) for item in self.value)
1191+
else:
1192+
return tuple(NumericArray(array) for array in self.value)
1193+
1194+
@property
1195+
def element_order(self) -> tuple:
1196+
return (
1197+
BASIC_ATOM_NUMERICARRAY_ELT_ORDER,
1198+
self.value.shape,
1199+
self.value.dtype,
1200+
self.value.tobytes()
1201+
)
1202+
1203+
@property
1204+
def pattern_precedence(self) -> tuple:
1205+
return super().pattern_precedence
1206+
1207+
def sameQ(self, rhs) -> bool:
1208+
return isinstance(rhs, NumericArray) and numpy.array_equal(self.value, rhs.value)
1209+
1210+
def to_sympy(self, **kwargs):
1211+
return None
1212+
1213+
# TODO: note that this returns a simple python list (of lists),
1214+
# not the numpy array - ok?
1215+
def to_python(self, *args, **kwargs):
1216+
return self.value.tolist()
1217+
1218+
# TODO: what is this? is it right?
1219+
def user_hash(self, update):
1220+
update(self._summary[2])
1221+
1222+
def __getnewargs__(self):
1223+
return (self.value, self.value.dtype)
1224+
1225+
11011226
class String(Atom, BoxElementMixin):
11021227
value: str
11031228
class_head_name = "System`String"

0 commit comments

Comments
 (0)