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__)