diff --git a/src/core/IronPython/Lib/iptest/numpy_shim.py b/src/core/IronPython/Lib/iptest/numpy_shim.py
new file mode 100644
index 000000000..5ce0f7fba
--- /dev/null
+++ b/src/core/IronPython/Lib/iptest/numpy_shim.py
@@ -0,0 +1,262 @@
+# Licensed to the .NET Foundation under one or more agreements.
+# The .NET Foundation licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information.
+
+"""A minimal stand-in for NumPy, for running tests ported from NumPy against CLR arrays.
+
+Ported tests import it as `from iptest import numpy_shim as np`. The NumPy-like functions that
+IronPython implements on `System.Array` are re-exported unchanged; everything else here (array
+creation, dtypes, `testing`) is a test helper written in Python, not a part of IronPython's API.
+
+Differences from NumPy that ported tests have to take into account:
+
+* dtypes are CLR types (`System.Type`); `dtype_of(x)` replaces `x.dtype`.
+* The default integer dtype is `BigInteger`, the CLR type of Python `int` (as in `Array[int]`).
+* There are no 0-d arrays.
+* Values of CLR types `Boolean`, `Int32`, `Double` and `Complex` are Python scalars, hence "weak"
+ in type promotion, unlike NumPy scalars `np.bool`, `np.int32`, `np.float64` and `np.complex128`.
+* Arrays are not views: functions returning arrays always return new arrays.
+* All arrays are writeable; there are no read-only arrays (no `flags.writeable`).
+* Operators `+` and `*` on arrays are not element-wise (in the default mode); use `add` and `multiply`.
+* `float16` (`System.Half`) is only available on .NET 7 and later (`HAS_FLOAT16`). Its values
+ are created with `clr.Convert(value, np.float16)`, as `System.Half` cannot be called with a number.
+"""
+
+import itertools
+import unittest
+
+import clr
+import System
+from System import Array
+
+# ---- NumPy-like functions implemented by IronPython
+
+add = Array.add
+multiply = Array.multiply
+
+# ---- dtypes
+
+bool_ = System.Boolean
+int8 = System.SByte
+int16 = System.Int16
+int32 = System.Int32
+int64 = System.Int64
+uint8 = System.Byte
+uint16 = System.UInt16
+uint32 = System.UInt32
+uint64 = System.UInt64
+float32 = System.Single
+float64 = System.Double
+complex128 = complex
+
+HAS_FLOAT16 = hasattr(System, 'Half')
+if HAS_FLOAT16:
+ float16 = System.Half
+
+_typecodes = {
+ '?': System.Boolean,
+ 'b': System.SByte, 'i1': System.SByte,
+ 'h': System.Int16, 'i2': System.Int16,
+ 'i': System.Int32, 'i4': System.Int32,
+ 'l': System.Int64, 'q': System.Int64, 'i8': System.Int64,
+ 'B': System.Byte, 'u1': System.Byte,
+ 'H': System.UInt16, 'u2': System.UInt16,
+ 'I': System.UInt32, 'u4': System.UInt32,
+ 'L': System.UInt64, 'Q': System.UInt64, 'u8': System.UInt64,
+ 'f': System.Single, 'f4': System.Single,
+ 'd': System.Double, 'f8': System.Double,
+ 'D': complex, 'c16': complex,
+ 'O': object,
+}
+if HAS_FLOAT16:
+ _typecodes.update({'e': float16, 'f2': float16})
+
+def dtype(obj):
+ """Return the CLR type for a dtype-like object: a type or a NumPy type code."""
+ if isinstance(obj, System.Type):
+ return obj
+ if isinstance(obj, str):
+ obj = _typecodes[obj]
+ return clr.GetClrType(obj)
+
+_dtype = dtype
+
+def dtype_of(x):
+ """Return the element type of an array, or the CLR type of a scalar (NumPy: `x.dtype`)."""
+ if isinstance(x, Array):
+ return x.GetType().GetElementType()
+ return x.GetType()
+
+# ---- array creation and inspection
+
+def shape(a):
+ if not isinstance(a, Array):
+ return ()
+ return tuple(a.GetLength(d) for d in range(a.Rank))
+
+_shape = shape
+
+def ndim(a):
+ return len(shape(a))
+
+def size(a):
+ res = 1
+ for n in shape(a):
+ res *= n
+ return res
+
+def ndindex(*shape):
+ if len(shape) == 1 and isinstance(shape[0], tuple):
+ shape = shape[0]
+ return itertools.product(*(range(n) for n in shape))
+
+def _setitem(a, idx, value):
+ # CLR arrays do not accept a tuple key in assignments, only separate indices (as in a[i, j] = value)
+ Array.__setitem__(a, *idx, value)
+
+def _normalize_shape(shape):
+ return (shape,) if isinstance(shape, int) else tuple(shape)
+
+def _infer_shape(obj):
+ if isinstance(obj, Array):
+ return _shape(obj)
+ if isinstance(obj, (list, tuple)):
+ return (len(obj),) + (_infer_shape(obj[0]) if obj else ())
+ return ()
+
+def _flatten(obj):
+ if isinstance(obj, Array):
+ return [obj[idx] for idx in ndindex(_shape(obj))]
+ if isinstance(obj, (list, tuple)):
+ return [item for sub in obj for item in _flatten(sub)]
+ return [obj]
+
+def _infer_dtype(obj):
+ """Infer the dtype of a new array from its contents, like NumPy does for Python scalars.
+
+ Nested arrays contribute their element type; Python scalars are typed by their kind.
+ """
+ kinds = (System.Boolean, int, System.Double, complex)
+ category = -1
+ clr_types = set()
+
+ def visit(o):
+ nonlocal category
+ if isinstance(o, Array):
+ clr_types.add(dtype_of(o))
+ elif isinstance(o, (list, tuple)):
+ for item in o:
+ visit(item)
+ elif isinstance(o, (System.Boolean, int, float, complex)):
+ # bool is also an int, so the first match is the narrowest kind
+ category = max(category, next(i for i, k in enumerate(kinds) if isinstance(o, k)))
+ else:
+ clr_types.add(clr.GetClrType(object if o is None else type(o)))
+
+ visit(obj)
+ if not clr_types:
+ return _dtype(kinds[category]) if category >= 0 else _dtype(float64)
+ if len(clr_types) == 1 and category < 0:
+ return clr_types.pop()
+ return clr.GetClrType(object)
+
+def empty(shape, dtype=float64):
+ shape = _normalize_shape(shape)
+ if not shape:
+ raise NotImplementedError("0-d arrays are not supported")
+ return Array.CreateInstance(_dtype(dtype), *shape)
+
+def full(shape, fill_value, dtype=None):
+ if dtype is None:
+ dtype = _infer_dtype(fill_value)
+ res = empty(shape, dtype)
+ for idx in ndindex(_shape(res)):
+ _setitem(res, idx, fill_value)
+ return res
+
+def zeros(shape, dtype=float64):
+ return full(shape, 0, dtype)
+
+def ones(shape, dtype=float64):
+ return full(shape, 1, dtype)
+
+def array(object, dtype=None):
+ """Create a new (0-based) array from an array or nested sequences of scalars."""
+ shape = _infer_shape(object)
+ if dtype is None:
+ dtype = _infer_dtype(object)
+ res = empty(shape, dtype)
+ for idx, item in zip(ndindex(shape), _flatten(object)):
+ _setitem(res, idx, item)
+ return res
+
+def asarray(a, dtype=None):
+ if isinstance(a, Array) and (dtype is None or _dtype(dtype) == dtype_of(a)):
+ return a
+ return array(a, dtype)
+
+def arange(stop, dtype=None):
+ return array(list(range(stop)), dtype)
+
+def reshape(a, newshape):
+ newshape = _normalize_shape(newshape)
+ res = empty(newshape, dtype_of(a))
+ for idx, item in zip(ndindex(newshape), _flatten(a)):
+ _setitem(res, idx, item)
+ return res
+
+def broadcast_to(a, shape):
+ a = asarray(a)
+ shape = _normalize_shape(shape)
+ ashape = _shape(a)
+ offset = len(shape) - len(ashape)
+ if offset < 0 or any(n != 1 and n != shape[d + offset] for d, n in enumerate(ashape)):
+ raise ValueError("cannot broadcast shape %r to %r" % (ashape, shape))
+ res = empty(shape, dtype_of(a))
+ for idx in ndindex(shape):
+ _setitem(res, idx, a[tuple(0 if n == 1 else idx[d + offset] for d, n in enumerate(ashape))])
+ return res
+
+def tolist(a):
+ """Convert an array to nested lists (NumPy: `a.tolist()`)."""
+ def sub(prefix, dims):
+ if not dims:
+ return a[prefix]
+ return [sub(prefix + (i,), dims[1:]) for i in range(dims[0])]
+ return sub((), _shape(a)) if isinstance(a, Array) else a
+
+# ---- numpy.testing
+
+class testing:
+ """The subset of `numpy.testing` used by the ported tests."""
+
+ @staticmethod
+ def assert_(val, msg=''):
+ if not val:
+ raise AssertionError(msg)
+
+ @staticmethod
+ def assert_equal(actual, desired, err_msg=''):
+ if isinstance(actual, Array) or isinstance(desired, Array):
+ testing.assert_array_equal(actual, desired, err_msg)
+ elif actual != desired:
+ raise AssertionError("%r != %r %s" % (actual, desired, err_msg))
+
+ @staticmethod
+ def assert_array_equal(actual, desired, err_msg=''):
+ """Compare shapes and elements; a scalar is compared with every element of the other side."""
+ ashape, dshape = _infer_shape(actual), _infer_shape(desired)
+ if ashape and dshape and ashape != dshape:
+ raise AssertionError("shapes %r and %r mismatch %s" % (ashape, dshape, err_msg))
+ aitems, ditems = _flatten(actual), _flatten(desired)
+ if not ashape:
+ aitems = aitems * len(ditems)
+ if not dshape:
+ ditems = ditems * len(aitems)
+ if aitems != ditems:
+ raise AssertionError("arrays are not equal: %r != %r %s" % (tolist(actual), tolist(desired), err_msg))
+
+ @staticmethod
+ def assert_raises(exception_class, *args, **kwargs):
+ """Call `args[0](*args[1:], **kwargs)` expecting an exception, or return a context manager."""
+ return unittest.TestCase().assertRaises(exception_class, *args, **kwargs)
diff --git a/src/core/IronPython/Runtime/Binding/PythonBinder.cs b/src/core/IronPython/Runtime/Binding/PythonBinder.cs
index 5b91b531d..132bb57b5 100644
--- a/src/core/IronPython/Runtime/Binding/PythonBinder.cs
+++ b/src/core/IronPython/Runtime/Binding/PythonBinder.cs
@@ -743,7 +743,7 @@ internal static void AssertNotExtensionType(Type t) {
res[typeof(DBNull)] = new Type[] { typeof(DBNullOps) };
res[typeof(List<>)] = new Type[] { typeof(ListOfTOps<>) };
res[typeof(Dictionary<,>)] = new Type[] { typeof(DictionaryOfTOps<,>) };
- res[typeof(Array)] = new Type[] { typeof(ArrayOps) };
+ res[typeof(Array)] = new Type[] { typeof(ArrayOps), typeof(NDArrayOps) };
res[typeof(Assembly)] = new Type[] { typeof(PythonAssemblyOps) };
res[typeof(Enum)] = new Type[] { typeof(EnumOps) };
res[typeof(Delegate)] = new Type[] { typeof(DelegateOps) };
diff --git a/src/core/IronPython/Runtime/Operations/NDArrayOps.cs b/src/core/IronPython/Runtime/Operations/NDArrayOps.cs
new file mode 100644
index 000000000..ef1b000fb
--- /dev/null
+++ b/src/core/IronPython/Runtime/Operations/NDArrayOps.cs
@@ -0,0 +1,481 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Numerics;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Microsoft.Scripting;
+using Microsoft.Scripting.Runtime;
+
+namespace IronPython.Runtime.Operations {
+ ///
+ /// Provides the NumPy-like interface of CLR arrays.
+ ///
+ ///
+ /// The functions are static methods of System.Array and follow the interface of the NumPy
+ /// functions of the same name (a subset of it). The operators of CLR arrays are in .
+ ///
+ /// The result element type follows NumPy's type promotion rules applied to the CLR primitive
+ /// numeric types (bool, signed and unsigned integers, Half on .NET 7+, Single,
+ /// Double), BigInteger (an integer wider than any other) and Complex.
+ /// Like in NumPy, operands are cast to a numeric result type before the operation is applied. Python scalars
+ /// (bool, int, float, complex) are "weak", as in NumPy 2:
+ /// they don't change the element type of the other operand unless their kind is higher.
+ /// Operations of other types produce object elements, unless both operands have the same
+ /// element type, which is str or Decimal.
+ ///
+ ///
+ public static class NDArrayOps {
+ #region Python APIs
+
+ [StaticExtensionMethod]
+ public static object? add(CodeContext context, object? x1, object? x2, [Optional] object? @out, [ParamDictionary] IDictionary kwargs)
+ => BinaryUFunc(context, nameof(add), ExpressionType.Add, x1, x2, @out, kwargs);
+
+ [StaticExtensionMethod]
+ public static object? multiply(CodeContext context, object? x1, object? x2, [Optional] object? @out, [ParamDictionary] IDictionary kwargs)
+ => BinaryUFunc(context, nameof(multiply), ExpressionType.Multiply, x1, x2, @out, kwargs);
+
+ #endregion
+
+ #region Internal APIs
+
+ ///
+ /// Applies a binary Python operator element-wise, broadcasting the operands.
+ ///
+ ///
+ /// When neither the operands nor are arrays, the result is a scalar.
+ ///
+ internal static object? BinaryUFunc(CodeContext context, string name, ExpressionType operation, object? x1, object? x2, object? @out, IDictionary kwargs) {
+ Type? dtype = null;
+ foreach (var kwarg in kwargs) {
+ switch (kwarg.Key) {
+ case "dtype":
+ if (kwarg.Value is not null) dtype = Converter.ConvertToType(kwarg.Value);
+ break;
+ default:
+ throw PythonOps.TypeError("{0}() got an unexpected keyword argument '{1}'", name, kwarg.Key);
+ }
+ }
+
+ Array? outArray = GetOutArray(@out);
+ CheckOperand(name, x1);
+ CheckOperand(name, x2);
+
+ Type resultType = dtype ?? ResultType(x1, x2);
+ if (dtype is not null) {
+ CheckCast(name, "input 0", GetElementType(x1), dtype);
+ CheckCast(name, "input 1", GetElementType(x2), dtype);
+ }
+ if (outArray is not null) {
+ CheckCast(name, "output", resultType, outArray.GetType().GetElementType()!);
+ }
+
+ var site = CallSite>.Create(context.LanguageContext.BinaryOperation(operation));
+
+ Array? a1 = x1 as Array;
+ Array? a2 = x2 as Array;
+ if (a1 is null && a2 is null && outArray is null && dtype is null && IsWeakScalar(x1) && IsWeakScalar(x2)) {
+ return site.Target(site, x1, x2);
+ }
+
+ // Like NumPy, operands are cast to a common numeric type before the operation is applied
+ bool cast1 = MustCastInput(x1, resultType);
+ bool cast2 = MustCastInput(x2, resultType);
+ if (a1 is null && cast1) x1 = ConvertElement(x1, resultType);
+ if (a2 is null && cast2) x2 = ConvertElement(x2, resultType);
+
+ if (a1 is null && a2 is null && outArray is null) {
+ return ConvertElement(site.Target(site, x1, x2), resultType);
+ }
+
+ int[] shape = BroadcastShapes(GetShape(x1), GetShape(x2))
+ ?? throw PythonOps.ValueError("operands could not be broadcast together with shapes {0} {1} ", FormatShape(GetShape(x1)), FormatShape(GetShape(x2)));
+
+ if (outArray is not null) {
+ int[] outShape = GetShape(outArray);
+ int[]? fullShape = BroadcastShapes(shape, outShape);
+ if (fullShape is null || !ShapeEquals(fullShape, outShape)) {
+ throw PythonOps.ValueError("non-broadcastable output operand with shape {0} doesn't match the broadcast shape {1}", FormatShape(outShape), FormatShape(shape));
+ }
+ shape = outShape;
+ }
+
+ int[] lowerBounds = GetLowerBounds(shape.Length, a1, a2, outArray);
+ Array result = outArray ?? CreateArray(resultType, shape, lowerBounds);
+ Type elementType = result.GetType().GetElementType()!;
+
+ int count = 1;
+ foreach (int len in shape) count *= len;
+ if (count == 0) return result;
+
+ int[] index = (int[])lowerBounds.Clone();
+ int[]? index1 = a1 is null ? null : new int[a1.Rank];
+ int[]? index2 = a2 is null ? null : new int[a2.Rank];
+ for (int i = 0; i < count; i++) {
+ object? item1 = a1 is null ? x1 : GetBroadcastValue(a1, index, index1!);
+ object? item2 = a2 is null ? x2 : GetBroadcastValue(a2, index, index2!);
+ if (a1 is not null && cast1) item1 = ConvertElement(item1, resultType);
+ if (a2 is not null && cast2) item2 = ConvertElement(item2, resultType);
+ object? res = site.Target(site, item1, item2);
+ if (dtype is not null && outArray is not null) res = ConvertElement(res, dtype);
+ result.SetValue(ConvertElement(res, elementType), index);
+
+ NextIndex(index, shape, lowerBounds);
+ }
+ return result;
+ }
+
+ ///
+ /// Returns the shape of an array, or an empty shape for a scalar.
+ ///
+ internal static int[] GetShape(object? x) {
+ if (x is not Array a) return [];
+
+ int[] shape = new int[a.Rank];
+ for (int d = 0; d < shape.Length; d++) {
+ shape[d] = a.GetLength(d);
+ }
+ return shape;
+ }
+
+ ///
+ /// Computes the shape that the given shapes broadcast to, or null if they are not broadcastable.
+ ///
+ ///
+ /// Shapes are aligned at the trailing dimension; a dimension of length 1, or a missing one,
+ /// stretches to the length of the corresponding dimension of the other shape.
+ ///
+ internal static int[]? BroadcastShapes(int[] shape1, int[] shape2) {
+ int rank = Math.Max(shape1.Length, shape2.Length);
+ int[] res = new int[rank];
+ for (int d = 0; d < rank; d++) {
+ int len1 = d < rank - shape1.Length ? 1 : shape1[d - rank + shape1.Length];
+ int len2 = d < rank - shape2.Length ? 1 : shape2[d - rank + shape2.Length];
+ if (len1 == len2 || len2 == 1) {
+ res[d] = len1;
+ } else if (len1 == 1) {
+ res[d] = len2;
+ } else {
+ return null;
+ }
+ }
+ return res;
+ }
+
+ ///
+ /// Creates an array of the given element type, shape and lower bounds.
+ ///
+ internal static Array CreateArray(Type elementType, int[] shape, int[] lowerBounds) {
+ foreach (int lb in lowerBounds) {
+ if (lb != 0) return Array.CreateInstance(elementType, shape, lowerBounds);
+ }
+ return Array.CreateInstance(elementType, shape);
+ }
+
+ ///
+ /// Determines the element type of the result of an operation on the given operands.
+ ///
+ ///
+ /// If all operands are weak scalars, the result is the default type of the highest kind among them.
+ ///
+ internal static Type ResultType(params object?[] operands) {
+ Type? res = null;
+ int weakCategory = 0;
+ foreach (object? x in operands) {
+ if (IsWeakScalar(x)) {
+ weakCategory = Math.Max(weakCategory, KindCategory(x!.GetType()));
+ } else {
+ Type type = GetElementType(x);
+ res = res is null ? type : PromoteTypes(res, type);
+ }
+ }
+ if (res is null) return DefaultType(weakCategory);
+
+ foreach (object? x in operands) {
+ if (IsWeakScalar(x)) res = PromoteWeakScalar(res, x!);
+ }
+ return res;
+ }
+
+ ///
+ /// Returns the element type of an array, or the type of a scalar.
+ ///
+ internal static Type GetElementType(object? x)
+ => x is Array a ? a.GetType().GetElementType()! : x?.GetType() ?? typeof(object);
+
+ ///
+ /// Determines the common element type of two element types.
+ ///
+ internal static Type PromoteTypes(Type type1, Type type2) {
+ if (type1 == type2) {
+ return TryGetNumericInfo(type1, out _, out _) || type1 == typeof(string) || type1 == typeof(decimal) ? type1 : typeof(object);
+ }
+ if (!TryGetNumericInfo(type1, out NumericKind kind1, out int size1) || !TryGetNumericInfo(type2, out NumericKind kind2, out int size2)) {
+ return typeof(object);
+ }
+
+ if (kind1 == NumericKind.Bool) return type2;
+ if (kind2 == NumericKind.Bool) return type1;
+ if (kind1 == NumericKind.Complex || kind2 == NumericKind.Complex) return typeof(Complex);
+
+ if (kind1 == NumericKind.Float || kind2 == NumericKind.Float) {
+ if (kind1 == kind2) return size1 >= size2 ? type1 : type2;
+
+ // a float combined with an integer, which needs a float twice its size to hold all its values
+ int floatSize = kind1 == NumericKind.Float ? size1 : size2;
+ int intSize = kind1 == NumericKind.Float ? size2 : size1;
+ return FloatType(Math.Max(floatSize, Math.Min(2 * intSize, 8)));
+ }
+
+ // both are integers
+ if (kind1 == kind2) return size1 >= size2 ? type1 : type2;
+
+ // a signed integer combined with an unsigned one
+ int signedSize = kind1 == NumericKind.Int ? size1 : size2;
+ int unsignedSize = kind1 == NumericKind.Int ? size2 : size1;
+ if (signedSize > unsignedSize) return kind1 == NumericKind.Int ? type1 : type2;
+ return unsignedSize switch {
+ 1 => typeof(short),
+ 2 => typeof(int),
+ 4 => typeof(long),
+ _ => typeof(double), // no signed integer can hold all values of UInt64
+ };
+ }
+
+ #endregion
+
+ #region Private helpers
+
+ private enum NumericKind {
+ Bool,
+ UInt,
+ Int,
+ Float,
+ Complex,
+ }
+
+ ///
+ /// Classifies a numeric element type by kind and size in bytes.
+ ///
+ ///
+ /// BigInteger is treated as a signed integer wider than any other.
+ /// Half (NumPy's float16) is supported on .NET 7 and later, which provide its arithmetic operators.
+ ///
+ private static bool TryGetNumericInfo(Type type, out NumericKind kind, out int size) {
+ switch (Type.GetTypeCode(type)) {
+ case TypeCode.Boolean: kind = NumericKind.Bool; size = 1; return true;
+ case TypeCode.SByte: kind = NumericKind.Int; size = 1; return true;
+ case TypeCode.Byte: kind = NumericKind.UInt; size = 1; return true;
+ case TypeCode.Int16: kind = NumericKind.Int; size = 2; return true;
+ case TypeCode.UInt16: kind = NumericKind.UInt; size = 2; return true;
+ case TypeCode.Int32: kind = NumericKind.Int; size = 4; return true;
+ case TypeCode.UInt32: kind = NumericKind.UInt; size = 4; return true;
+ case TypeCode.Int64: kind = NumericKind.Int; size = 8; return true;
+ case TypeCode.UInt64: kind = NumericKind.UInt; size = 8; return true;
+ case TypeCode.Single: kind = NumericKind.Float; size = 4; return true;
+ case TypeCode.Double: kind = NumericKind.Float; size = 8; return true;
+ }
+ if (type == typeof(BigInteger)) {
+ kind = NumericKind.Int; size = 16; return true;
+ }
+ if (type == typeof(Complex)) {
+ kind = NumericKind.Complex; size = 16; return true;
+ }
+#if NET7_0_OR_GREATER
+ if (type == typeof(Half)) {
+ kind = NumericKind.Float; size = 2; return true;
+ }
+#endif
+ kind = default; size = 0;
+ return false;
+ }
+
+ ///
+ /// Returns the floating-point type of the given size in bytes.
+ ///
+ private static Type FloatType(int size) => size switch {
+#if NET7_0_OR_GREATER
+ 2 => typeof(Half),
+#endif
+ 4 => typeof(float),
+ _ => typeof(double),
+ };
+
+ ///
+ /// Checks whether an operand has to be cast to the numeric type in which the operation is computed.
+ ///
+ private static bool MustCastInput(object? x, Type type)
+ => GetElementType(x) != type && TryGetNumericInfo(type, out _, out _);
+
+ ///
+ /// Checks whether the operand is a Python bool, int, float or complex.
+ ///
+ private static bool IsWeakScalar(object? x)
+ => x is bool || x is int || x is BigInteger || x is double || x is Complex;
+
+ ///
+ /// Determines the element type of an operation of a weak scalar with an operand of the given element type.
+ ///
+ private static Type PromoteWeakScalar(Type type, object scalar) {
+ if (!TryGetNumericInfo(type, out _, out _)) return typeof(object);
+
+ int scalarCategory = KindCategory(scalar.GetType());
+ return scalarCategory <= KindCategory(type) ? type : DefaultType(scalarCategory);
+ }
+
+ ///
+ /// Orders numeric types by kind: bool < integers < floats < complex.
+ ///
+ /// The category of the kind, or -1 if the type is not numeric.
+ private static int KindCategory(Type type) {
+ if (!TryGetNumericInfo(type, out NumericKind kind, out _)) return -1;
+
+ return kind switch {
+ NumericKind.Bool => 0,
+ NumericKind.UInt => 1,
+ NumericKind.Int => 1,
+ NumericKind.Float => 2,
+ _ => 3,
+ };
+ }
+
+ ///
+ /// Returns the element type for a Python scalar of the given kind category.
+ ///
+ ///
+ /// For integers, it is BigInteger, the CLR type of Python int (as in Array[int]).
+ ///
+ private static Type DefaultType(int category) => category switch {
+ 0 => typeof(bool),
+ 1 => typeof(BigInteger),
+ 2 => typeof(double),
+ _ => typeof(Complex),
+ };
+
+ ///
+ /// Checks that a value of one type can be cast to another with NumPy's same_kind casting rule.
+ ///
+ ///
+ /// Numeric types can be cast to any numeric type of the same or a higher kind, and any type
+ /// can be cast to itself or to object.
+ ///
+ private static void CheckCast(string name, string operand, Type from, Type to) {
+ if (from == to || to == typeof(object)) return;
+
+ int fromCategory = KindCategory(from);
+ int toCategory = KindCategory(to);
+ if (fromCategory < 0 || toCategory < fromCategory) {
+ throw PythonOps.TypeError("Cannot cast ufunc '{0}' {1} from {2} to {3} with casting rule 'same_kind'",
+ name, operand, PythonOps.GetPythonTypeNameFromType(from), PythonOps.GetPythonTypeNameFromType(to));
+ }
+ }
+
+ private static void CheckOperand(string name, object? x) {
+ if (x is PythonList || x is PythonTuple) {
+ throw PythonOps.TypeError("{0}() does not support operands of type '{1}'; convert them to System.Array", name, PythonOps.GetPythonTypeName(x));
+ }
+ }
+
+ private static Array? GetOutArray(object? @out) {
+ if (@out is PythonTuple tuple) {
+ if (tuple.Count != 1) throw PythonOps.ValueError("The 'out' tuple must have exactly one entry per ufunc output");
+ @out = tuple[0];
+ }
+ return @out switch {
+ null or Missing => null,
+ Array a => a,
+ _ => throw PythonOps.TypeError("return arrays must be of ArrayType"),
+ };
+ }
+
+ ///
+ /// Determines the lower bounds of the result of an operation on the given arrays.
+ ///
+ ///
+ /// Dimensions of the arrays are aligned at the trailing dimension, and aligned dimensions must
+ /// have the same lower bound.
+ ///
+ private static int[] GetLowerBounds(int rank, params Array?[] arrays) {
+ int[] lowerBounds = new int[rank];
+ bool[] isSet = new bool[rank];
+ foreach (Array? a in arrays) {
+ if (a is null) continue;
+ int offset = rank - a.Rank;
+ for (int d = 0; d < a.Rank; d++) {
+ int lb = a.GetLowerBound(d);
+ if (!isSet[d + offset]) {
+ lowerBounds[d + offset] = lb;
+ isSet[d + offset] = true;
+ } else if (lowerBounds[d + offset] != lb) {
+ throw PythonOps.ValueError("operands have different lower bounds in aligned dimensions");
+ }
+ }
+ }
+ return lowerBounds;
+ }
+
+ ///
+ /// Gets the element of that broadcasts to the given index of the result.
+ ///
+ /// The array, of a rank not higher than the length of .
+ /// The index into the result.
+ /// An index buffer of the rank of .
+ private static object? GetBroadcastValue(Array a, int[] index, int[] buffer) {
+ int offset = index.Length - buffer.Length;
+ for (int d = 0; d < buffer.Length; d++) {
+ buffer[d] = a.GetLength(d) == 1 ? a.GetLowerBound(d) : index[d + offset];
+ }
+ return a.GetValue(buffer);
+ }
+
+ ///
+ /// Advances the index to the next element in row-major order.
+ ///
+ private static void NextIndex(int[] index, int[] shape, int[] lowerBounds) {
+ for (int d = index.Length - 1; d >= 0; d--) {
+ if (++index[d] < lowerBounds[d] + shape[d]) return;
+ index[d] = lowerBounds[d];
+ }
+ }
+
+ private static object? ConvertElement(object? value, Type type) {
+ if (type == typeof(object)) return value;
+ if (type == typeof(bool) && value is int i) return ScriptingRuntimeHelpers.BooleanToObject(i != 0); // NumPy: add is or, multiply is and
+ if (value is bool b && type != typeof(bool)) value = b ? 1 : 0; // not every numeric type converts from bool, e.g. Half
+ return Converter.Convert(value!, type);
+ }
+
+ private static bool ShapeEquals(int[] shape1, int[] shape2) {
+ if (shape1.Length != shape2.Length) return false;
+ for (int d = 0; d < shape1.Length; d++) {
+ if (shape1[d] != shape2[d]) return false;
+ }
+ return true;
+ }
+
+ ///
+ /// Formats a shape like NumPy does in error messages, e.g. (2,3) or (2,).
+ ///
+ private static string FormatShape(int[] shape) {
+ var sb = new StringBuilder("(");
+ for (int d = 0; d < shape.Length; d++) {
+ if (d > 0) sb.Append(',');
+ sb.Append(shape[d]);
+ }
+ if (shape.Length == 1) sb.Append(',');
+ return sb.Append(')').ToString();
+ }
+
+ #endregion
+ }
+}
diff --git a/tests/suite/ndarray/LICENSE_NUMPY.txt b/tests/suite/ndarray/LICENSE_NUMPY.txt
new file mode 100644
index 000000000..f37a12cc4
--- /dev/null
+++ b/tests/suite/ndarray/LICENSE_NUMPY.txt
@@ -0,0 +1,30 @@
+Copyright (c) 2005-2025, NumPy Developers.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of the NumPy Developers nor the names of any
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/tests/suite/ndarray/test_numeric.py b/tests/suite/ndarray/test_numeric.py
new file mode 100644
index 000000000..0aaa9eaf1
--- /dev/null
+++ b/tests/suite/ndarray/test_numeric.py
@@ -0,0 +1,102 @@
+# Ported from NumPy v2.5.4, numpy/_core/tests/test_numeric.py, and adapted to CLR arrays.
+# Copyright (c) 2005-2025, NumPy Developers.
+# Licensed under the BSD 3-Clause License; see LICENSE_NUMPY.txt in this directory.
+#
+# Adaptations:
+# * pytest-style classes derive from IronPythonTestCase.
+# * `x.dtype` is spelled `np.dtype_of(x)`; element-wise `*` is spelled `np.multiply`.
+# * Types without a CLR equivalent (longdouble, complex64, clongdouble) are left out; float16 (System.Half)
+# is tested only where available, and its values are created with clr.Convert.
+# * CLR values of types Boolean, Int32, Double and Complex are Python scalars, which are weak in type
+# promotion, unlike NumPy scalars of the same types. Where the original test uses such a NumPy
+# scalar as a strong operand, a 1-element array is used instead.
+# * np.result_type is not implemented yet, so test_result_type is left out.
+
+from iptest import IronPythonTestCase, is_cli, run_test, skipUnlessIronPython
+
+if is_cli:
+ import clr
+ from iptest import numpy_shim as np
+ assert_equal = np.testing.assert_equal
+
+
+@skipUnlessIronPython()
+class TestTypes(IronPythonTestCase):
+ def check_promotion_cases(self, promote_func):
+ # tests that the scalars get coerced correctly.
+ b = np.array([False]) # np.bool(0)
+ i8, i16, i32, i64 = np.int8(0), np.int16(0), np.array([0], np.int32), np.int64(0)
+ u8, u16, u32, u64 = np.uint8(0), np.uint16(0), np.uint32(0), np.uint64(0)
+ f32, f64 = np.float32(0), np.array([0], np.float64)
+ c128 = np.array([0], np.complex128)
+
+ # coercion within the same kind
+ assert_equal(promote_func(i8, i16), np.dtype(np.int16))
+ assert_equal(promote_func(i32, i8), np.dtype(np.int32))
+ assert_equal(promote_func(i16, i64), np.dtype(np.int64))
+ assert_equal(promote_func(u8, u32), np.dtype(np.uint32))
+ assert_equal(promote_func(f32, f64), np.dtype(np.float64))
+
+ # coercion between kinds
+ assert_equal(promote_func(b, i32), np.dtype(np.int32))
+ assert_equal(promote_func(b, u8), np.dtype(np.uint8))
+ assert_equal(promote_func(i8, u8), np.dtype(np.int16))
+ assert_equal(promote_func(u8, i32), np.dtype(np.int32))
+ assert_equal(promote_func(i64, u32), np.dtype(np.int64))
+ assert_equal(promote_func(u64, i32), np.dtype(np.float64))
+ assert_equal(promote_func(i32, f32), np.dtype(np.float64))
+ assert_equal(promote_func(i64, f32), np.dtype(np.float64))
+ assert_equal(promote_func(f32, i16), np.dtype(np.float32))
+ assert_equal(promote_func(f32, u32), np.dtype(np.float64))
+ assert_equal(promote_func(c128, f32), np.dtype(np.complex128))
+
+ # coercion between scalars and 1-D arrays
+ assert_equal(promote_func(np.array([b]), i8), np.dtype(np.int8))
+ assert_equal(promote_func(np.array([b]), u8), np.dtype(np.uint8))
+ assert_equal(promote_func(np.array([b]), i32), np.dtype(np.int32))
+ assert_equal(promote_func(np.array([b]), u32), np.dtype(np.uint32))
+ assert_equal(promote_func(np.array([i8]), i64), np.dtype(np.int64))
+ # unsigned and signed unfortunately tend to promote to float64:
+ assert_equal(promote_func(u64, np.array([i32])), np.dtype(np.float64))
+ assert_equal(promote_func(i64, np.array([u32])), np.dtype(np.int64))
+ assert_equal(promote_func(np.array([u16]), i32), np.dtype(np.int32))
+ assert_equal(promote_func(np.array([-1], np.int32), np.array([u64])),
+ np.dtype(np.float64))
+ assert_equal(promote_func(f64, np.array([f32])), np.dtype(np.float64))
+ assert_equal(promote_func(np.array([f32]), c128),
+ np.dtype(np.complex128))
+
+ # coercion between scalars and 1-D arrays, where
+ # the scalar has greater kind than the array
+ assert_equal(promote_func(np.array([b]), f64), np.dtype(np.float64))
+ assert_equal(promote_func(np.array([b]), i64), np.dtype(np.int64))
+ assert_equal(promote_func(np.array([b]), u64), np.dtype(np.uint64))
+ assert_equal(promote_func(np.array([i8]), f64), np.dtype(np.float64))
+ assert_equal(promote_func(np.array([u16]), f64), np.dtype(np.float64))
+
+ def test_coercion(self):
+ def res_type(a, b):
+ return np.dtype_of(np.add(a, b))
+
+ self.check_promotion_cases(res_type)
+
+ # Use-case: float/complex scalar * bool/int8 array
+ # shouldn't narrow the float/complex type
+ for a in [np.array([True, False]), np.array([-3, 12], dtype=np.int8)]:
+ b = np.multiply(1.234, a)
+ assert_equal(np.dtype_of(b), np.dtype('f8'), f"array type {np.dtype_of(a)}")
+ b = np.multiply(np.float64(1.234), a)
+ assert_equal(np.dtype_of(b), np.dtype('f8'), f"array type {np.dtype_of(a)}")
+ b = np.multiply(np.float32(1.234), a)
+ assert_equal(np.dtype_of(b), np.dtype('f4'), f"array type {np.dtype_of(a)}")
+ if np.HAS_FLOAT16:
+ b = np.multiply(clr.Convert(1.234, np.float16), a)
+ assert_equal(np.dtype_of(b), np.dtype('f2'), f"array type {np.dtype_of(a)}")
+
+ b = np.multiply(1.234j, a)
+ assert_equal(np.dtype_of(b), np.dtype('c16'), f"array type {np.dtype_of(a)}")
+ b = np.multiply(np.complex128(1.234j), a)
+ assert_equal(np.dtype_of(b), np.dtype('c16'), f"array type {np.dtype_of(a)}")
+
+
+run_test(__name__)
diff --git a/tests/suite/ndarray/test_ufunc.py b/tests/suite/ndarray/test_ufunc.py
new file mode 100644
index 000000000..b626c311f
--- /dev/null
+++ b/tests/suite/ndarray/test_ufunc.py
@@ -0,0 +1,65 @@
+# Ported from NumPy v2.5.4, numpy/_core/tests/test_ufunc.py, and adapted to CLR arrays.
+# Copyright (c) 2005-2025, NumPy Developers.
+# Licensed under the BSD 3-Clause License; see LICENSE_NUMPY.txt in this directory.
+#
+# Adaptations:
+# * pytest-style classes derive from IronPythonTestCase; parametrized tests are loops with subTest.
+# * 0-d arrays are replaced with scalars; cases that need a 0-d output are left out.
+# * Element-wise `*` and `==` are spelled with function calls and `assert_array_equal`.
+# * Functions not implemented yet (positive, vecdot) are left out.
+
+from iptest import IronPythonTestCase, is_cli, run_test, skipUnlessIronPython
+
+if is_cli:
+ from iptest import numpy_shim as np
+ assert_raises = np.testing.assert_raises
+ assert_array_equal = np.testing.assert_array_equal
+
+
+@skipUnlessIronPython()
+class TestUfuncKwargs(IronPythonTestCase):
+ def test_kwarg_exact(self):
+ assert_raises(TypeError, np.add, 1, 2, castingx='safe')
+ assert_raises(TypeError, np.add, 1, 2, dtypex=int)
+ assert_raises(TypeError, np.add, 1, 2, extobjx=[4096])
+ assert_raises(TypeError, np.add, 1, 2, outx=None)
+ assert_raises(TypeError, np.add, 1, 2, sigx='ii->i')
+ assert_raises(TypeError, np.add, 1, 2, signaturex='ii->i')
+ assert_raises(TypeError, np.add, 1, 2, subokx=False)
+ assert_raises(TypeError, np.add, 1, 2, wherex=[True])
+
+ def test_sig_signature(self):
+ assert_raises(TypeError, np.add, 1, 2, sig='ii->i',
+ signature='ii->i')
+
+ def test_sig_dtype(self):
+ assert_raises(TypeError, np.add, 1, 2, sig='ii->i',
+ dtype=int)
+ assert_raises(TypeError, np.add, 1, 2, signature='ii->i',
+ dtype=int)
+
+
+@skipUnlessIronPython()
+class TestUfunc(IronPythonTestCase):
+ def test_out_broadcasts(self):
+ # For ufuncs and gufuncs (not for reductions), we currently allow
+ # the output to cause broadcasting of the input arrays.
+ # both along dimensions with shape 1 and dimensions which do not
+ # exist at all in the inputs.
+ arr = np.reshape(np.arange(3), (1, 3))
+ out = np.empty((5, 4, 3))
+ np.add(arr, arr, out=out)
+ assert_array_equal(out, np.broadcast_to(np.multiply(np.arange(3), 2), (5, 4, 3)))
+
+ def test_out_broadcast_errors(self):
+ # Output is (currently) allowed to broadcast inputs, but it cannot be
+ # smaller than the actual result.
+ for arr, out in [
+ ([1, 2], np.empty(1)),
+ (np.ones((4, 3)), np.empty((4, 1)))]:
+ with self.subTest(arr=arr, out=np.shape(out)):
+ with self.assertRaisesRegex(ValueError, "non-broadcastable"):
+ np.add(1.0, np.asarray(arr), out=out)
+
+
+run_test(__name__)
diff --git a/tests/suite/ndarray/test_umath.py b/tests/suite/ndarray/test_umath.py
new file mode 100644
index 000000000..a01f8a606
--- /dev/null
+++ b/tests/suite/ndarray/test_umath.py
@@ -0,0 +1,42 @@
+# Ported from NumPy v2.5.4, numpy/_core/tests/test_umath.py, and adapted to CLR arrays.
+# Copyright (c) 2005-2025, NumPy Developers.
+# Licensed under the BSD 3-Clause License; see LICENSE_NUMPY.txt in this directory.
+#
+# Adaptations:
+# * pytest-style classes derive from IronPythonTestCase.
+# * 0-d arrays are replaced with 1-element arrays.
+# * Parameters that IronPython does not support (subok) and functions not implemented yet (frexp)
+# are left out, as are the checks of read-only arrays (CLR arrays are always writeable).
+
+from iptest import IronPythonTestCase, is_cli, run_test, skipUnlessIronPython
+
+if is_cli:
+ from iptest import numpy_shim as np
+ assert_ = np.testing.assert_
+ assert_raises = np.testing.assert_raises
+
+
+@skipUnlessIronPython()
+class TestOut(IronPythonTestCase):
+ def test_out_subok(self):
+ a = np.array([0.5])
+ o = np.empty((1,))
+
+ r = np.add(a, 2, o)
+ assert_(r is o)
+ r = np.add(a, 2, out=o)
+ assert_(r is o)
+ r = np.add(a, 2, out=(o,))
+ assert_(r is o)
+
+ assert_raises(TypeError, np.add, a, 2, o, o)
+ assert_raises(TypeError, np.add, a, 2, o, out=o)
+ assert_raises(TypeError, np.add, a, 2, None, out=o)
+ assert_raises(ValueError, np.add, a, 2, out=(o, o))
+ assert_raises(ValueError, np.add, a, 2, out=())
+ assert_raises(TypeError, np.add, a, 2, [])
+ assert_raises(TypeError, np.add, a, 2, out=[])
+ assert_raises(TypeError, np.add, a, 2, out=([],))
+
+
+run_test(__name__)
diff --git a/tests/suite/test_ndarray.py b/tests/suite/test_ndarray.py
new file mode 100644
index 000000000..8895ca911
--- /dev/null
+++ b/tests/suite/test_ndarray.py
@@ -0,0 +1,242 @@
+# Licensed to the .NET Foundation under one or more agreements.
+# The .NET Foundation licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information.
+
+##
+## Test the NumPy-like interface of CLR arrays (System.Array)
+##
+## Tests ported from NumPy are in ndarray/.
+##
+
+import unittest
+
+from iptest import IronPythonTestCase, is_cli, run_test, skipUnlessIronPython
+
+if is_cli:
+ import clr
+ import System
+
+def array2d(elemType, rows, bases=None):
+ a = System.Array.CreateInstance(elemType, (len(rows), len(rows[0])), bases or (0, 0))
+ for i, row in enumerate(rows):
+ for j, item in enumerate(row):
+ a[i + a.GetLowerBound(0), j + a.GetLowerBound(1)] = item
+ return a
+
+@skipUnlessIronPython()
+class NDArrayFunctionsTest(IronPythonTestCase):
+
+ def test_add_multiply_issue_examples(self):
+ # https://github.com/IronLanguages/ironpython3/issues/1887
+ add, multiply = System.Array.add, System.Array.multiply
+ Array = System.Array
+
+ self.assertEqual(add(Array[int]([1, 2]), 2), Array[int]([3, 4]))
+ self.assertEqual(add(Array[int]([1, 2]), Array[int]([3, 4])), Array[int]([4, 6]))
+ self.assertEqual(add(array2d(int, [[1, 2], [3, 4]]), Array[int]([10, 20])), array2d(int, [[11, 22], [13, 24]]))
+
+ self.assertEqual(multiply(Array[int]([1, 2]), 2), Array[int]([2, 4]))
+ self.assertEqual(multiply(Array[int]([1, 2]), Array[int]([3, 4])), Array[int]([3, 8]))
+ self.assertEqual(multiply(array2d(int, [[1, 2], [3, 4]]), Array[int]([10, 20])), array2d(int, [[10, 40], [30, 80]]))
+
+ def test_add_broadcasting(self):
+ add, Array = System.Array.add, System.Array
+
+ col = array2d(int, [[1], [2]])
+ self.assertEqual(add(col, Array[int]([10, 20, 30])), array2d(int, [[11, 21, 31], [12, 22, 32]]))
+ self.assertEqual(add(Array[int]([10, 20, 30]), col), array2d(int, [[11, 21, 31], [12, 22, 32]]))
+ self.assertEqual(add(1, Array[int]([1, 2])), Array[int]([2, 3]))
+
+ a3 = System.Array.CreateInstance(int, 2, 1, 3)
+ r = add(a3, array2d(int, [[1, 2, 3], [4, 5, 6]]))
+ self.assertEqual([r.GetLength(d) for d in range(r.Rank)], [2, 2, 3])
+ self.assertEqual(r[1, 1, 2], 6)
+
+ with self.assertRaisesRegex(ValueError, r"^operands could not be broadcast together with shapes \(2,\) \(3,\) $"):
+ add(Array[int]([1, 2]), Array[int]([1, 2, 3]))
+ with self.assertRaisesRegex(ValueError, r"shapes \(2,1\) \(3,2\)"):
+ add(col, array2d(int, [[1, 2], [3, 4], [5, 6]]))
+
+ empty = System.Array.CreateInstance(int, 0)
+ self.assertEqual(add(empty, 1), empty)
+ self.assertEqual(add(System.Array.CreateInstance(int, 0, 2), Array[int]([1, 2])).GetLength(1), 2)
+
+ def test_add_scalars(self):
+ add = System.Array.add
+
+ self.assertEqual(add(2, 3), 5)
+ self.assertEqual(add(2.5, 1), 3.5)
+ self.assertEqual(add('a', 'b'), 'ab')
+ # big Python ints are not converted
+ self.assertEqual(add(2**100, 1), 2**100 + 1)
+ # a CLR scalar of a type that is not a Python scalar type determines the result type
+ r = add(System.Int64(1), 2)
+ self.assertEqual(r, 3)
+ self.assertIsInstance(r, System.Int64)
+ self.assertIsInstance(add(System.Int16(1), System.Single(2)), System.Single)
+ self.assertIsInstance(add(2, 3, dtype=float), float)
+
+ def test_add_result_type(self):
+ add, multiply, Array = System.Array.add, System.Array.multiply, System.Array
+ BigInteger, Complex = int, complex # their CLR types are System.Numerics.BigInteger and Complex
+
+ def elemType(a):
+ return a.GetType().GetElementType()
+
+ for x1, x2, expected in [
+ (Array[System.Int16]([1]), Array[System.Single]([1]), System.Single),
+ (Array[System.Int32]([1]), Array[System.Single]([1]), System.Double),
+ (Array[System.UInt64]([1]), Array[System.Int32]([1]), System.Double),
+ (Array[System.UInt64]([1]), Array[BigInteger]([1]), BigInteger),
+ (Array[System.Byte]([1]), Array[System.SByte]([1]), System.Int16),
+ (Array[System.UInt32]([1]), Array[System.Int32]([1]), System.Int64),
+ (Array[bool]([True]), Array[System.Byte]([1]), System.Byte),
+ (Array[System.Single]([1]), Array[Complex]([1]), Complex),
+ (Array[System.Byte]([1]), 1, System.Byte), # weak int
+ (Array[System.Int32]([1]), 2**20, System.Int32), # weak int
+ (Array[System.Single]([1]), 1.5, System.Single), # weak float
+ (Array[System.Int16]([1]), 1.5, System.Double), # weak float of higher kind
+ (Array[System.Single]([1]), 1j, Complex), # weak complex of higher kind
+ (Array[bool]([True]), 1, BigInteger), # weak int of higher kind: Python int
+ (Array[bool]([True]), True, System.Boolean),
+ (Array[str](['a']), 'b', System.String),
+ (Array[System.Decimal]([1]), Array[System.Decimal]([1]), System.Decimal),
+ (Array[System.Decimal]([1]), 1, object),
+ (Array[System.Decimal]([1]), Array[int]([1]), object),
+ (Array[object]([1]), 1, object),
+ (Array[System.TimeSpan]([System.TimeSpan.Zero]), Array[System.TimeSpan]([System.TimeSpan.Zero]), object),
+ ]:
+ with self.subTest(x1=x1, x2=x2):
+ self.assertEqual(elemType(add(x1, x2)), clr.GetClrType(expected))
+ self.assertEqual(elemType(add(x2, x1)), clr.GetClrType(expected))
+
+ # NumPy: add is logical or, multiply is logical and on bools
+ t, f = Array[bool]([True, True, False, False]), Array[bool]([True, False, True, False])
+ self.assertEqual(add(t, f), Array[bool]([True, True, True, False]))
+ self.assertEqual(multiply(t, f), Array[bool]([True, False, False, False]))
+
+ self.assertEqual(add(Array[str](['a', 'b']), 'c'), Array[str](['ac', 'bc']))
+ self.assertEqual(add(Array[object]([[1], 'a']), Array[object]([[2], 'b'])), Array[object]([[1, 2], 'ab']))
+
+ @unittest.skipUnless(is_cli and hasattr(System, 'Half'), "System.Half requires .NET 7 or later")
+ def test_add_half(self):
+ add, multiply, Array = System.Array.add, System.Array.multiply, System.Array
+ Half = System.Half
+
+ def elemType(a):
+ return a.GetType().GetElementType()
+
+ # NumPy's float16
+ for x2, expected in [
+ (Array[Half]([1]), Half),
+ (Array[System.SByte]([1]), Half),
+ (Array[System.Byte]([1]), Half),
+ (Array[System.Int16]([1]), System.Single),
+ (Array[System.Int32]([1]), System.Double),
+ (Array[System.Single]([1]), System.Single),
+ (Array[bool]([True]), Half),
+ (1, Half), # weak int
+ (1.5, Half), # weak float
+ (1j, complex), # weak complex of higher kind
+ ]:
+ with self.subTest(x2=x2):
+ self.assertEqual(elemType(add(Array[Half]([1]), x2)), clr.GetClrType(expected))
+ self.assertEqual(elemType(add(x2, Array[Half]([1]))), clr.GetClrType(expected))
+
+ # operands are cast to Half before the operation, which IronPython supports only between Half values
+ self.assertEqual(add(Array[Half]([1.5, 2]), 1), Array[Half]([2.5, 3]))
+ self.assertEqual(multiply(Array[Half]([1.5, 2]), Array[System.Byte]([2, 3])), Array[Half]([3, 6]))
+ self.assertEqual(add(Array[Half]([1.5]), 1, dtype=float), Array[float]([2.5]))
+ self.assertIsInstance(add(clr.Convert(1.5, Half), 1), Half)
+
+ def test_add_overflow(self):
+ add, multiply, Array = System.Array.add, System.Array.multiply, System.Array
+
+ # unlike NumPy, values that do not fit into the result type raise an error rather than wrap around
+ self.assertRaises(OverflowError, add, Array[System.Int32]([2**31 - 1]), 1)
+ self.assertRaises(OverflowError, add, Array[System.Byte]([200]), 100)
+ self.assertRaises(OverflowError, add, Array[System.Byte]([0]), -1)
+ self.assertRaises(OverflowError, multiply, Array[System.Int64]([2**62]), 2)
+ self.assertEqual(add(Array[int]([2**31 - 1]), 1), Array[int]([2**31]))
+
+ def test_add_dtype(self):
+ add, Array = System.Array.add, System.Array
+
+ self.assertEqual(add(Array[int]([1, 2]), 1, dtype=float), Array[float]([2.0, 3.0]))
+ self.assertEqual(add(Array[int]([1, 2]), 1, dtype=System.Int16), Array[System.Int16]([2, 3]))
+ self.assertEqual(add(Array[int]([1, 2]), 1, dtype=None), Array[int]([2, 3]))
+ self.assertEqual(add(Array[int]([1, 2]), 1, dtype=object), Array[object]([2, 3]))
+ self.assertRaises(OverflowError, add, Array[int]([1, 2]), 255, dtype=System.Byte)
+
+ # NumPy's casting rule 'same_kind'
+ with self.assertRaisesRegex(TypeError, r"^Cannot cast ufunc 'add' input 0 from float to int with casting rule 'same_kind'$"):
+ add(Array[float]([1.5]), 1, dtype=int)
+ with self.assertRaisesRegex(TypeError, r"input 1 from float to int"):
+ add(Array[int]([1]), 1.5, dtype=int)
+ self.assertRaises(TypeError, add, Array[int]([1]), 1, dtype=bool)
+ self.assertRaises(TypeError, add, Array[str](['a']), 'b', dtype=int)
+ self.assertRaises(TypeError, add, Array[int]([1]), 1, dtype=str)
+
+ self.assertRaises(TypeError, add, Array[int]([1]), 1, typo=None)
+
+ def test_add_out(self):
+ add, Array = System.Array.add, System.Array
+
+ out = Array[float]([0.0, 0.0])
+ self.assertIs(add(Array[int]([1, 2]), 1, out), out)
+ self.assertEqual(out, Array[float]([2.0, 3.0]))
+ self.assertIs(add(Array[int]([1, 2]), 2, out=out), out)
+ self.assertEqual(out, Array[float]([3.0, 4.0]))
+ self.assertIs(add(Array[int]([1, 2]), 3, out=(out,)), out)
+ self.assertEqual(out, Array[float]([4.0, 5.0]))
+ self.assertIs(add(1, 2, out=out), out)
+ self.assertEqual(out, Array[float]([3.0, 3.0]))
+ self.assertEqual(add(Array[int]([1, 2]), 1, out=None), Array[int]([2, 3]))
+ self.assertEqual(add(Array[int]([1, 2]), 1, out=(None,)), Array[int]([2, 3]))
+
+ # out may be of a lower type of the same kind, and broadcasts the operands
+ out = System.Array.CreateInstance(System.Int16, 2, 2)
+ add(Array[int]([1, 2]), 1, out=out)
+ self.assertEqual(out, array2d(System.Int16, [[2, 3], [2, 3]]))
+
+ with self.assertRaisesRegex(TypeError, r"^Cannot cast ufunc 'add' output from float to Int16 with casting rule 'same_kind'$"):
+ add(Array[int]([1, 2]), 1.5, out=out)
+ with self.assertRaisesRegex(ValueError, r"^non-broadcastable output operand with shape \(1,\) doesn't match the broadcast shape \(2,\)$"):
+ add(Array[int]([1, 2]), 1, out=Array[int]([0]))
+ self.assertRaises(TypeError, add, Array[int]([1, 2]), 1, out=[0, 0])
+ self.assertRaises(ValueError, add, Array[int]([1, 2]), 1, out=(None, None))
+
+ def test_add_lower_bounds(self):
+ add, Array = System.Array.add, System.Array
+
+ a = Array[int]((1, 2), base=1)
+ self.assertEqual(add(a, 1), Array[int]((2, 3), base=1))
+ self.assertEqual(add(a, a), Array[int]((2, 4), base=1))
+ self.assertEqual(add(a, Array[int]((10,), base=1)), Array[int]((11, 12), base=1))
+
+ m = array2d(int, [[1, 2], [3, 4]], bases=(5, 1))
+ r = add(m, a)
+ self.assertEqual([r.GetLowerBound(0), r.GetLowerBound(1)], [5, 1])
+ self.assertEqual(r, array2d(int, [[2, 4], [4, 6]], bases=(5, 1)))
+
+ with self.assertRaisesRegex(ValueError, "lower bounds"):
+ add(a, Array[int]([1, 2]))
+ with self.assertRaisesRegex(ValueError, "lower bounds"):
+ add(a, 1, out=Array[int]([0, 0]))
+
+ def test_add_unsupported_operands(self):
+ add, Array = System.Array.add, System.Array
+
+ # NumPy converts sequences to arrays; this is not supported yet
+ self.assertRaises(TypeError, add, Array[int]([1, 2]), [1, 2])
+ self.assertRaises(TypeError, add, (1, 2), Array[int]([1, 2]))
+ self.assertRaises(TypeError, add, Array[int]([1]), None)
+ self.assertRaises(TypeError, add, Array[int]([1]))
+
+ def test_operators_unchanged(self):
+ # operators + and * keep their legacy semantics (concatenation and repetition)
+ a = System.Array[int]([1, 2])
+ self.assertEqual(a + a, System.Array[int]([1, 2, 1, 2]))
+ self.assertEqual(a * 2, System.Array[int]([1, 2, 1, 2]))
+
+run_test(__name__)