diff --git a/src/NumSharp.Core/Logic/np.find_common_type.cs b/src/NumSharp.Core/Logic/np.find_common_type.cs
index ebd3f380..64ff6b4c 100644
--- a/src/NumSharp.Core/Logic/np.find_common_type.cs
+++ b/src/NumSharp.Core/Logic/np.find_common_type.cs
@@ -8,6 +8,82 @@
namespace NumSharp
{
+ // ================================================================================
+ // TYPE PROMOTION SYSTEM
+ // ================================================================================
+ //
+ // This file implements NumPy-compatible type promotion for arithmetic operations.
+ // When two arrays (or an array and a scalar) are combined, this system determines
+ // the result dtype.
+ //
+ // ARCHITECTURE
+ // ============
+ //
+ // Four lookup tables are used (two pairs for Type and NPTypeCode access):
+ //
+ // _typemap_arr_arr / _nptypemap_arr_arr - Array + Array promotion
+ // _typemap_arr_scalar / _nptypemap_arr_scalar - Array + Scalar promotion
+ //
+ // The tables are FrozenDictionary<(T1, T2), TResult> for O(1) lookup.
+ //
+ // WHEN EACH TABLE IS USED
+ // =======================
+ //
+ // The _FindCommonType(NDArray, NDArray) method decides which table to use:
+ //
+ // if (both are non-scalar arrays) → _typemap_arr_arr
+ // if (both are scalar arrays) → _FindCommonScalarType (uses arr_arr rules)
+ // if (one is array, one is scalar) → _typemap_arr_scalar
+ //
+ // This matters because scalar promotion follows different rules than array promotion.
+ //
+ // KIND HIERARCHY
+ // ==============
+ //
+ // Types are grouped into "kinds" with a promotion hierarchy:
+ //
+ // boolean < integer < floating-point < complex
+ //
+ // When operands are of different kinds, the result promotes to the higher kind:
+ //
+ // int32 + float32 → float64 (int promotes to float)
+ // float32 + complex → complex (float promotes to complex)
+ //
+ // WITHIN-KIND PROMOTION
+ // =====================
+ //
+ // When operands are the same kind, promotion depends on the operation type:
+ //
+ // Array + Array (both non-scalar):
+ // - Result is the "larger" type that can hold both ranges
+ // - uint8 + int16 → int16 (int16 can hold uint8 range + negatives)
+ // - uint32 + int32 → int64 (need 64-bit to hold both ranges)
+ // - uint64 + int64 → float64 (no integer type can hold both!)
+ //
+ // Array + Scalar (NEP 50 behavior):
+ // - Array dtype wins when scalar is same-kind (e.g., both integers)
+ // - uint8_array + int32_scalar → uint8 (array wins)
+ // - float32_array + int32_scalar → float32 (array wins, same effective kind)
+ //
+ // EXAMPLES
+ // ========
+ //
+ // var a = np.array(new byte[] {1, 2, 3}); // uint8
+ // var b = np.array(new int[] {4, 5, 6}); // int32
+ //
+ // (a + b).dtype == np.int32 // arr+arr: promotes to int32
+ // (a + 5).dtype == np.uint8 // arr+scalar: array wins (NEP 50)
+ // (a + 5.0).dtype == np.float64 // cross-kind: float wins
+ //
+ // REFERENCES
+ // ==========
+ //
+ // - NumPy type promotion: https://numpy.org/doc/stable/reference/ufuncs.html#type-casting-rules
+ // - NEP 50 (scalar promotion): https://numpy.org/neps/nep-0050-scalar-promotion.html
+ // - Array API type promotion: https://data-apis.org/array-api/latest/API_specification/type_promotion.html
+ //
+ // ================================================================================
+
[SuppressMessage("ReSharper", "StaticMemberInitializerReferesToMemberBelow")]
public static partial class np
{
@@ -50,6 +126,39 @@ static np()
{
#region arr_arr
+ // ============================================================================
+ // ARRAY-ARRAY TYPE PROMOTION TABLE
+ // ============================================================================
+ //
+ // This table defines type promotion when TWO ARRAYS are combined.
+ // The key is (LeftArrayType, RightArrayType), the value is the result type.
+ //
+ // PROMOTION RULES:
+ //
+ // 1. Same type: result is that type
+ // int32 + int32 → int32
+ //
+ // 2. Same kind, different size: result is larger type
+ // int16 + int32 → int32
+ // float32 + float64 → float64
+ //
+ // 3. Signed + Unsigned (same size): result is next-larger signed type
+ // int16 + uint16 → int32 (need more bits for both ranges)
+ // int32 + uint32 → int64
+ // int64 + uint64 → float64 (no larger integer exists!)
+ //
+ // 4. Cross-kind: result is the higher kind
+ // int32 + float32 → float64 (int32 needs float64 precision)
+ // uint8 + float32 → float32 (uint8 fits in float32)
+ //
+ // 5. Complex: absorbs everything
+ // float32 + complex64 → complex64
+ // int32 + complex64 → complex128 (int32 needs float64 precision)
+ //
+ // This table matches NumPy 2.x arr+arr behavior exactly.
+ //
+ // ============================================================================
+
var typemap_arr_arr = new Dictionary<(Type, Type), Type>(180);
typemap_arr_arr.Add((np.@bool, np.@bool), np.@bool);
typemap_arr_arr.Add((np.@bool, np.uint8), np.uint8);
@@ -243,6 +352,45 @@ static np()
#region arr_scalar
+ // ============================================================================
+ // ARRAY-SCALAR TYPE PROMOTION TABLE
+ // ============================================================================
+ //
+ // This table defines type promotion when an array operates with a scalar value.
+ // The key is (ArrayType, ScalarType), the value is the result type.
+ //
+ // NUMSHARP DESIGN DECISION:
+ // C# primitive scalars (int, short, long, etc.) are treated as "weakly typed"
+ // like Python scalars in NumPy 2.x, NOT like NumPy scalars (np.int32, etc.).
+ //
+ // This means: np.array(new byte[]{1,2,3}) + 5 → uint8 result (not int32)
+ //
+ // WHY: This matches the natural Python/NumPy user experience where `arr + 5`
+ // preserves the array's dtype when both are integers. This is consistent with
+ // NumPy 2.x behavior under NEP 50 for Python scalar operands.
+ //
+ // NEP 50 (NumPy Enhancement Proposal 50):
+ // https://numpy.org/neps/nep-0050-scalar-promotion.html
+ //
+ // Key rule: When an array operates with a scalar of the same "kind" (e.g., both
+ // are integers), the array dtype wins. Cross-kind operations (int + float) still
+ // promote to the higher kind (float).
+ //
+ // AFFECTED ENTRIES (12 total - all unsigned array + signed scalar):
+ //
+ // | Array Type | Scalar Types | NumPy 1.x Result | NumPy 2.x Result |
+ // |------------|-------------------|------------------|------------------|
+ // | uint8 | int16/int32/int64 | int16/int32/int64| uint8 |
+ // | uint16 | int16/int32/int64 | int32/int32/int64| uint16 |
+ // | uint32 | int16/int32/int64 | int64/int64/int64| uint32 |
+ // | uint64 | int16/int32/int64 | float64 (!) | uint64 |
+ //
+ // Verified against NumPy 2.4.2:
+ // >>> (np.array([1,2,3], np.uint8) + 5).dtype
+ // dtype('uint8')
+ //
+ // ============================================================================
+
var typemap_arr_scalar = new Dictionary<(Type, Type), Type>();
typemap_arr_scalar.Add((np.@bool, np.@bool), np.@bool);
typemap_arr_scalar.Add((np.@bool, np.uint8), np.uint8);
@@ -259,11 +407,11 @@ static np()
typemap_arr_scalar.Add((np.uint8, np.@bool), np.uint8);
typemap_arr_scalar.Add((np.uint8, np.uint8), np.uint8);
typemap_arr_scalar.Add((np.uint8, np.@char), np.uint8);
- typemap_arr_scalar.Add((np.uint8, np.int16), np.int16);
+ typemap_arr_scalar.Add((np.uint8, np.int16), np.uint8);
typemap_arr_scalar.Add((np.uint8, np.uint16), np.uint8);
- typemap_arr_scalar.Add((np.uint8, np.int32), np.int32);
+ typemap_arr_scalar.Add((np.uint8, np.int32), np.uint8);
typemap_arr_scalar.Add((np.uint8, np.uint32), np.uint8);
- typemap_arr_scalar.Add((np.uint8, np.int64), np.int64);
+ typemap_arr_scalar.Add((np.uint8, np.int64), np.uint8);
typemap_arr_scalar.Add((np.uint8, np.uint64), np.uint8);
typemap_arr_scalar.Add((np.uint8, np.float32), np.float32);
typemap_arr_scalar.Add((np.uint8, np.float64), np.float64);
@@ -298,11 +446,11 @@ static np()
typemap_arr_scalar.Add((np.uint16, np.@bool), np.uint16);
typemap_arr_scalar.Add((np.uint16, np.uint8), np.uint16);
typemap_arr_scalar.Add((np.uint16, np.@char), np.uint16);
- typemap_arr_scalar.Add((np.uint16, np.int16), np.int32);
+ typemap_arr_scalar.Add((np.uint16, np.int16), np.uint16);
typemap_arr_scalar.Add((np.uint16, np.uint16), np.uint16);
- typemap_arr_scalar.Add((np.uint16, np.int32), np.int32);
+ typemap_arr_scalar.Add((np.uint16, np.int32), np.uint16);
typemap_arr_scalar.Add((np.uint16, np.uint32), np.uint16);
- typemap_arr_scalar.Add((np.uint16, np.int64), np.int64);
+ typemap_arr_scalar.Add((np.uint16, np.int64), np.uint16);
typemap_arr_scalar.Add((np.uint16, np.uint64), np.uint16);
typemap_arr_scalar.Add((np.uint16, np.float32), np.float32);
typemap_arr_scalar.Add((np.uint16, np.float64), np.float64);
@@ -324,11 +472,11 @@ static np()
typemap_arr_scalar.Add((np.uint32, np.@bool), np.uint32);
typemap_arr_scalar.Add((np.uint32, np.uint8), np.uint32);
typemap_arr_scalar.Add((np.uint32, np.@char), np.uint32);
- typemap_arr_scalar.Add((np.uint32, np.int16), np.int64);
+ typemap_arr_scalar.Add((np.uint32, np.int16), np.uint32);
typemap_arr_scalar.Add((np.uint32, np.uint16), np.uint32);
- typemap_arr_scalar.Add((np.uint32, np.int32), np.int64);
+ typemap_arr_scalar.Add((np.uint32, np.int32), np.uint32);
typemap_arr_scalar.Add((np.uint32, np.uint32), np.uint32);
- typemap_arr_scalar.Add((np.uint32, np.int64), np.int64);
+ typemap_arr_scalar.Add((np.uint32, np.int64), np.uint32);
typemap_arr_scalar.Add((np.uint32, np.uint64), np.uint32);
typemap_arr_scalar.Add((np.uint32, np.float32), np.float64);
typemap_arr_scalar.Add((np.uint32, np.float64), np.float64);
@@ -350,11 +498,11 @@ static np()
typemap_arr_scalar.Add((np.uint64, np.@bool), np.uint64);
typemap_arr_scalar.Add((np.uint64, np.uint8), np.uint64);
typemap_arr_scalar.Add((np.uint64, np.@char), np.uint64);
- typemap_arr_scalar.Add((np.uint64, np.int16), np.float64);
+ typemap_arr_scalar.Add((np.uint64, np.int16), np.uint64);
typemap_arr_scalar.Add((np.uint64, np.uint16), np.uint64);
- typemap_arr_scalar.Add((np.uint64, np.int32), np.float64);
+ typemap_arr_scalar.Add((np.uint64, np.int32), np.uint64);
typemap_arr_scalar.Add((np.uint64, np.uint32), np.uint64);
- typemap_arr_scalar.Add((np.uint64, np.int64), np.float64);
+ typemap_arr_scalar.Add((np.uint64, np.int64), np.uint64);
typemap_arr_scalar.Add((np.uint64, np.uint64), np.uint64);
typemap_arr_scalar.Add((np.uint64, np.float32), np.float64);
typemap_arr_scalar.Add((np.uint64, np.float64), np.float64);
diff --git a/test/NumSharp.UnitTest/Logic/NEP50.cs b/test/NumSharp.UnitTest/Logic/NEP50.cs
new file mode 100644
index 00000000..3f46decd
--- /dev/null
+++ b/test/NumSharp.UnitTest/Logic/NEP50.cs
@@ -0,0 +1,964 @@
+using System;
+using AwesomeAssertions;
+using NumSharp.Backends;
+
+namespace NumSharp.UnitTest.Logic;
+
+///
+/// NEP 50 Type Promotion Tests
+///
+/// NumPy 2.0 introduced NEP 50 (https://numpy.org/neps/nep-0050-scalar-promotion.html)
+/// which changed scalar-array type promotion rules.
+///
+/// KEY CONCEPT: Python scalars (int, float, complex) are "weakly typed" - they adopt
+/// the array's dtype when combined. NumPy scalars (np.int32, np.float64) are "strongly
+/// typed" - their dtype is honored in promotion.
+///
+/// NUMSHARP DESIGN DECISION: C# primitive scalars (int, double, etc.) are treated as
+/// "weakly typed" like Python scalars, not like NumPy scalars. This gives users the
+/// natural Python-like experience:
+///
+/// np.array(new byte[]{1,2,3}) + 5 → uint8 result (not int32)
+///
+/// This matches how Python users expect `arr + 5` to behave in NumPy 2.x.
+///
+/// Each test is verified against NumPy 2.4.2 output.
+///
+public class NEP50_TypePromotion
+{
+ #region 1. Unsigned Array + Python Int (array dtype wins)
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.uint8) + 5).dtype)"
+ /// Output: uint8
+ ///
+ [Test]
+ public void UInt8Array_Plus_PythonInt_Returns_UInt8()
+ {
+ var arr = np.array(new byte[] { 1, 2, 3 });
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.uint8);
+ result.GetAtIndex(0).Should().Be(6);
+ result.GetAtIndex(1).Should().Be(7);
+ result.GetAtIndex(2).Should().Be(8);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.uint16) + 5).dtype)"
+ /// Output: uint16
+ ///
+ [Test]
+ public void UInt16Array_Plus_PythonInt_Returns_UInt16()
+ {
+ var arr = np.array(new ushort[] { 1, 2, 3 });
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.uint16);
+ result.GetAtIndex(0).Should().Be(6);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.uint32) + 5).dtype)"
+ /// Output: uint32
+ ///
+ [Test]
+ public void UInt32Array_Plus_PythonInt_Returns_UInt32()
+ {
+ var arr = np.array(new uint[] { 1, 2, 3 });
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.uint32);
+ result.GetAtIndex(0).Should().Be(6);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.uint64) + 5).dtype)"
+ /// Output: uint64
+ ///
+ [Test]
+ public void UInt64Array_Plus_PythonInt_Returns_UInt64()
+ {
+ var arr = np.array(new ulong[] { 1, 2, 3 });
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.uint64);
+ result.GetAtIndex(0).Should().Be(6);
+ }
+
+ #endregion
+
+ #region 2. Signed Array + Python Int (array dtype wins)
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.int16) + 5).dtype)"
+ /// Output: int16
+ ///
+ [Test]
+ public void Int16Array_Plus_PythonInt_Returns_Int16()
+ {
+ var arr = np.array(new short[] { 1, 2, 3 });
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.int16);
+ result.GetAtIndex(0).Should().Be(6);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.int32) + 5).dtype)"
+ /// Output: int32
+ ///
+ [Test]
+ public void Int32Array_Plus_PythonInt_Returns_Int32()
+ {
+ var arr = np.array(new int[] { 1, 2, 3 });
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.int32);
+ result.GetAtIndex(0).Should().Be(6);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.int64) + 5).dtype)"
+ /// Output: int64
+ ///
+ [Test]
+ public void Int64Array_Plus_PythonInt_Returns_Int64()
+ {
+ var arr = np.array(new long[] { 1, 2, 3 });
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.int64);
+ result.GetAtIndex(0).Should().Be(6);
+ }
+
+ #endregion
+
+ #region 3. Float Array + Python Int (int adopts float kind)
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.float32) + 5).dtype)"
+ /// Output: float32
+ ///
+ [Test]
+ public void Float32Array_Plus_PythonInt_Returns_Float32()
+ {
+ var arr = np.array(new float[] { 1f, 2f, 3f });
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.float32);
+ result.GetAtIndex(0).Should().Be(6f);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.float64) + 5).dtype)"
+ /// Output: float64
+ ///
+ [Test]
+ public void Float64Array_Plus_PythonInt_Returns_Float64()
+ {
+ var arr = np.array(new double[] { 1.0, 2.0, 3.0 });
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.float64);
+ result.GetAtIndex(0).Should().Be(6.0);
+ }
+
+ #endregion
+
+ #region 4. Float Array + Python Float (scalar adopts array dtype)
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.float32) + 5.0).dtype)"
+ /// Output: float32
+ ///
+ [Test]
+ public void Float32Array_Plus_PythonFloat_Returns_Float32()
+ {
+ var arr = np.array(new float[] { 1f, 2f, 3f });
+ var result = arr + 5.0;
+
+ result.dtype.Should().Be(np.float32);
+ result.GetAtIndex(0).Should().Be(6f);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.float64) + 5.0).dtype)"
+ /// Output: float64
+ ///
+ [Test]
+ public void Float64Array_Plus_PythonFloat_Returns_Float64()
+ {
+ var arr = np.array(new double[] { 1.0, 2.0, 3.0 });
+ var result = arr + 5.0;
+
+ result.dtype.Should().Be(np.float64);
+ result.GetAtIndex(0).Should().Be(6.0);
+ }
+
+ #endregion
+
+ #region 5. Int Array + Python Float (promotes to float64 - cross-kind)
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.int32) + 5.0).dtype)"
+ /// Output: float64
+ ///
+ [Test]
+ public void Int32Array_Plus_PythonFloat_Returns_Float64()
+ {
+ var arr = np.array(new int[] { 1, 2, 3 });
+ var result = arr + 5.0;
+
+ result.dtype.Should().Be(np.float64);
+ result.GetAtIndex(0).Should().Be(6.0);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.uint8) + 5.0).dtype)"
+ /// Output: float64
+ ///
+ [Test]
+ public void UInt8Array_Plus_PythonFloat_Returns_Float64()
+ {
+ var arr = np.array(new byte[] { 1, 2, 3 });
+ var result = arr + 5.0;
+
+ result.dtype.Should().Be(np.float64);
+ result.GetAtIndex(0).Should().Be(6.0);
+ }
+
+ #endregion
+
+ #region 6. Subtraction (same rules apply)
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([10,20,30], np.uint8) - 5).dtype)"
+ /// Output: uint8
+ ///
+ [Test]
+ public void UInt8Array_Minus_PythonInt_Returns_UInt8()
+ {
+ var arr = np.array(new byte[] { 10, 20, 30 });
+ var result = arr - 5;
+
+ result.dtype.Should().Be(np.uint8);
+ result.GetAtIndex(0).Should().Be(5);
+ result.GetAtIndex(1).Should().Be(15);
+ result.GetAtIndex(2).Should().Be(25);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([10,20,30], np.uint16) - 5).dtype)"
+ /// Output: uint16
+ ///
+ [Test]
+ public void UInt16Array_Minus_PythonInt_Returns_UInt16()
+ {
+ var arr = np.array(new ushort[] { 10, 20, 30 });
+ var result = arr - 5;
+
+ result.dtype.Should().Be(np.uint16);
+ result.GetAtIndex(0).Should().Be(5);
+ }
+
+ #endregion
+
+ #region 7. Multiplication (same rules apply)
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.uint8) * 5).dtype)"
+ /// Output: uint8
+ ///
+ [Test]
+ public void UInt8Array_Times_PythonInt_Returns_UInt8()
+ {
+ var arr = np.array(new byte[] { 1, 2, 3 });
+ var result = arr * 5;
+
+ result.dtype.Should().Be(np.uint8);
+ result.GetAtIndex(0).Should().Be(5);
+ result.GetAtIndex(1).Should().Be(10);
+ result.GetAtIndex(2).Should().Be(15);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.uint32) * 5).dtype)"
+ /// Output: uint32
+ ///
+ [Test]
+ public void UInt32Array_Times_PythonInt_Returns_UInt32()
+ {
+ var arr = np.array(new uint[] { 1, 2, 3 });
+ var result = arr * 5;
+
+ result.dtype.Should().Be(np.uint32);
+ result.GetAtIndex(0).Should().Be(5);
+ }
+
+ #endregion
+
+ #region 8. Division
+
+ ///
+ /// NumPy: python3 -c "import numpy as np; print((np.array([10,20,30], np.uint8) / 5).dtype)"
+ /// Output: float64
+ ///
+ /// NumSharp DIFFERENCE: Uses integer division (like C#'s / operator for integers).
+ /// NumSharp returns array dtype, not float64.
+ ///
+ [Test]
+ [Misaligned]
+ public void UInt8Array_Divide_PythonInt_Returns_UInt8_NumSharpBehavior()
+ {
+ var arr = np.array(new byte[] { 10, 20, 30 });
+ var result = arr / 5;
+
+ // NumSharp uses integer division, preserving array dtype
+ // NumPy would return float64
+ result.dtype.Should().Be(np.uint8, "NumSharp uses integer division");
+ result.GetAtIndex(0).Should().Be(2); // 10 / 5 = 2
+ }
+
+ ///
+ /// NumPy: python3 -c "import numpy as np; print((np.array([10,20,30], np.int32) / 5).dtype)"
+ /// Output: float64
+ ///
+ /// NumSharp DIFFERENCE: Uses integer division.
+ ///
+ [Test]
+ [Misaligned]
+ public void Int32Array_Divide_PythonInt_Returns_Int32_NumSharpBehavior()
+ {
+ var arr = np.array(new int[] { 10, 20, 30 });
+ var result = arr / 5;
+
+ // NumSharp uses integer division, preserving array dtype
+ result.dtype.Should().Be(np.int32, "NumSharp uses integer division");
+ result.GetAtIndex(0).Should().Be(2); // 10 / 5 = 2
+ }
+
+ ///
+ /// Float division works correctly.
+ /// Verified: python3 -c "import numpy as np; print((np.array([10,20,30], np.float64) / 5).dtype)"
+ /// Output: float64
+ ///
+ [Test]
+ public void Float64Array_Divide_PythonInt_Returns_Float64()
+ {
+ var arr = np.array(new double[] { 10.0, 20.0, 30.0 });
+ var result = arr / 5;
+
+ result.dtype.Should().Be(np.float64);
+ result.GetAtIndex(0).Should().Be(2.0);
+ }
+
+ #endregion
+
+ #region 9. Modulo
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([10,20,30], np.uint8) % 7).dtype)"
+ /// Output: uint8
+ ///
+ [Test]
+ public void UInt8Array_Mod_PythonInt_Returns_UInt8()
+ {
+ var arr = np.array(new byte[] { 10, 20, 30 });
+ var result = arr % 7;
+
+ result.dtype.Should().Be(np.uint8);
+ result.GetAtIndex(0).Should().Be(3); // 10 % 7 = 3
+ result.GetAtIndex(1).Should().Be(6); // 20 % 7 = 6
+ result.GetAtIndex(2).Should().Be(2); // 30 % 7 = 2
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([10,20,30], np.int32) % 7).dtype)"
+ /// Output: int32
+ ///
+ [Test]
+ public void Int32Array_Mod_PythonInt_Returns_Int32()
+ {
+ var arr = np.array(new int[] { 10, 20, 30 });
+ var result = arr % 7;
+
+ result.dtype.Should().Be(np.int32);
+ result.GetAtIndex(0).Should().Be(3);
+ }
+
+ #endregion
+
+ #region 10. Scalar-First Operations (commutative - same result)
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((5 + np.array([1,2,3], np.uint8)).dtype)"
+ /// Output: uint8
+ ///
+ [Test]
+ public void PythonInt_Plus_UInt8Array_Returns_UInt8()
+ {
+ var arr = np.array(new byte[] { 1, 2, 3 });
+ var result = 5 + arr;
+
+ result.dtype.Should().Be(np.uint8);
+ result.GetAtIndex(0).Should().Be(6);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((5 - np.array([1,2,3], np.uint8)).dtype)"
+ /// Output: uint8
+ ///
+ [Test]
+ public void PythonInt_Minus_UInt8Array_Returns_UInt8()
+ {
+ var arr = np.array(new byte[] { 1, 2, 3 });
+ var result = 5 - arr;
+
+ result.dtype.Should().Be(np.uint8);
+ result.GetAtIndex(0).Should().Be(4); // 5 - 1 = 4
+ result.GetAtIndex(1).Should().Be(3); // 5 - 2 = 3
+ result.GetAtIndex(2).Should().Be(2); // 5 - 3 = 2
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((5 * np.array([1,2,3], np.uint8)).dtype)"
+ /// Output: uint8
+ ///
+ [Test]
+ public void PythonInt_Times_UInt8Array_Returns_UInt8()
+ {
+ var arr = np.array(new byte[] { 1, 2, 3 });
+ var result = 5 * arr;
+
+ result.dtype.Should().Be(np.uint8);
+ result.GetAtIndex(0).Should().Be(5);
+ }
+
+ #endregion
+
+ #region 11. _FindCommonArrayScalarType Direct Tests
+
+ ///
+ /// Direct verification of the type promotion table for NEP 50 changes.
+ /// These are the 12 entries that changed from NumPy 1.x to 2.x behavior.
+ ///
+ [Test]
+ public void ArrayScalarType_UInt8_SignedScalars()
+ {
+ // NEP 50: uint8 array + signed scalar → uint8 (array wins)
+ np._FindCommonArrayScalarType(NPTypeCode.Byte, NPTypeCode.Int16).Should().Be(NPTypeCode.Byte);
+ np._FindCommonArrayScalarType(NPTypeCode.Byte, NPTypeCode.Int32).Should().Be(NPTypeCode.Byte);
+ np._FindCommonArrayScalarType(NPTypeCode.Byte, NPTypeCode.Int64).Should().Be(NPTypeCode.Byte);
+ }
+
+ [Test]
+ public void ArrayScalarType_UInt16_SignedScalars()
+ {
+ // NEP 50: uint16 array + signed scalar → uint16 (array wins)
+ np._FindCommonArrayScalarType(NPTypeCode.UInt16, NPTypeCode.Int16).Should().Be(NPTypeCode.UInt16);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt16, NPTypeCode.Int32).Should().Be(NPTypeCode.UInt16);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt16, NPTypeCode.Int64).Should().Be(NPTypeCode.UInt16);
+ }
+
+ [Test]
+ public void ArrayScalarType_UInt32_SignedScalars()
+ {
+ // NEP 50: uint32 array + signed scalar → uint32 (array wins)
+ np._FindCommonArrayScalarType(NPTypeCode.UInt32, NPTypeCode.Int16).Should().Be(NPTypeCode.UInt32);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt32, NPTypeCode.Int32).Should().Be(NPTypeCode.UInt32);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt32, NPTypeCode.Int64).Should().Be(NPTypeCode.UInt32);
+ }
+
+ [Test]
+ public void ArrayScalarType_UInt64_SignedScalars()
+ {
+ // NEP 50: uint64 array + signed scalar → uint64 (array wins)
+ np._FindCommonArrayScalarType(NPTypeCode.UInt64, NPTypeCode.Int16).Should().Be(NPTypeCode.UInt64);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt64, NPTypeCode.Int32).Should().Be(NPTypeCode.UInt64);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt64, NPTypeCode.Int64).Should().Be(NPTypeCode.UInt64);
+ }
+
+ #endregion
+
+ #region 12. Array-Array Operations (unchanged - uses arr_arr table)
+
+ ///
+ /// Array-array operations use _typemap_arr_arr, not _typemap_arr_scalar.
+ /// These should NOT be affected by NEP 50 changes to arr_scalar table.
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1], np.uint8) + np.array([5], np.int32)).dtype)"
+ /// Output: int32
+ ///
+ [Test]
+ public void ArrayArray_UInt8_Plus_Int32_Returns_Int32()
+ {
+ var arr1 = np.array(new byte[] { 1, 2, 3 });
+ var arr2 = np.array(new int[] { 5, 6, 7 });
+ var result = arr1 + arr2;
+
+ result.dtype.Should().Be(np.int32);
+ result.GetAtIndex(0).Should().Be(6);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1], np.uint16) + np.array([5], np.int32)).dtype)"
+ /// Output: int32
+ ///
+ [Test]
+ public void ArrayArray_UInt16_Plus_Int32_Returns_Int32()
+ {
+ var arr1 = np.array(new ushort[] { 1, 2, 3 });
+ var arr2 = np.array(new int[] { 5, 6, 7 });
+ var result = arr1 + arr2;
+
+ result.dtype.Should().Be(np.int32);
+ }
+
+ #endregion
+
+ #region 13. Unchanged Behaviors (sanity checks)
+
+ ///
+ /// Same-type operations remain unchanged.
+ ///
+ [Test]
+ public void SameType_UInt8_Plus_UInt8_Returns_UInt8()
+ {
+ var arr1 = np.array(new byte[] { 1, 2, 3 });
+ var arr2 = np.array(new byte[] { 4, 5, 6 });
+ var result = arr1 + arr2;
+
+ result.dtype.Should().Be(np.uint8);
+ result.GetAtIndex(0).Should().Be(5);
+ }
+
+ ///
+ /// Unsigned array + larger unsigned scalar → array dtype (unchanged).
+ ///
+ [Test]
+ public void UInt8Array_Plus_UInt32Scalar_Returns_UInt8()
+ {
+ // This was already "array wins" in NumPy 1.x for same-kind
+ np._FindCommonArrayScalarType(NPTypeCode.Byte, NPTypeCode.UInt32).Should().Be(NPTypeCode.Byte);
+ }
+
+ ///
+ /// Float operations with int scalars → float (array kind wins).
+ ///
+ [Test]
+ public void Float32Array_Operations_PreserveFloat()
+ {
+ var arr = np.array(new float[] { 1f, 2f, 3f });
+
+ (arr + 5).dtype.Should().Be(np.float32);
+ (arr - 5).dtype.Should().Be(np.float32);
+ (arr * 5).dtype.Should().Be(np.float32);
+ (5 + arr).dtype.Should().Be(np.float32);
+ }
+
+ #endregion
+
+ #region 14. Edge Cases
+
+ ///
+ /// Empty array operations should preserve dtype.
+ ///
+ [Test]
+ public void EmptyArray_Operations_PreserveDtype()
+ {
+ var arr = np.array(Array.Empty());
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.uint8);
+ result.size.Should().Be(0);
+ }
+
+ ///
+ /// 1D array operations.
+ ///
+ [Test]
+ public void OneDimensional_UInt8_Operations()
+ {
+ var arr = np.arange(10).astype(np.uint8);
+ var result = arr + 100;
+
+ result.dtype.Should().Be(np.uint8);
+ result.shape.Should().BeEquivalentTo(new[] { 10 });
+ }
+
+ ///
+ /// Multi-dimensional array operations.
+ ///
+ [Test]
+ public void MultiDimensional_UInt8_Operations()
+ {
+ var arr = np.arange(12).astype(np.uint8).reshape(3, 4);
+ var result = arr + 5;
+
+ result.dtype.Should().Be(np.uint8);
+ result.shape.Should().BeEquivalentTo(new[] { 3, 4 });
+ }
+
+ ///
+ /// Scalar array (0-d) operations use SCALAR-SCALAR promotion, not ARR-SCALAR.
+ ///
+ /// When both operands are scalars, NumSharp uses _FindCommonScalarType,
+ /// which follows different rules than _FindCommonArrayScalarType.
+ ///
+ /// NumPy: python3 -c "import numpy as np; print((np.uint8(10) + 5).dtype)"
+ /// Output: uint8 (NEP 50: weak scalar adopts stronger scalar dtype)
+ ///
+ /// NumSharp uses scalar-scalar table which may differ.
+ ///
+ [Test]
+ [Misaligned]
+ public void ScalarArray_Operations_UsesScalarScalarPromotion()
+ {
+ var arr = NDArray.Scalar((byte)10);
+ var result = arr + 5;
+
+ // Note: Scalar + scalar uses _FindCommonScalarType, not _FindCommonArrayScalarType
+ // Current behavior returns int32 (matches C# semantics)
+ result.dtype.Should().Be(np.int32,
+ "Scalar-scalar promotion follows different rules than arr-scalar");
+ }
+
+ #endregion
+
+ #region 15. Power Operation
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.uint8) ** 2).dtype)"
+ /// Output: uint8
+ ///
+ [Test]
+ public void UInt8Array_Power_PythonInt_Returns_UInt8()
+ {
+ var arr = np.array(new byte[] { 1, 2, 3 });
+ var result = np.power(arr, 2);
+
+ result.dtype.Should().Be(np.uint8);
+ result.GetAtIndex(0).Should().Be(1);
+ result.GetAtIndex(1).Should().Be(4);
+ result.GetAtIndex(2).Should().Be(9);
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.int32) ** 2).dtype)"
+ /// Output: int32
+ ///
+ [Test]
+ public void Int32Array_Power_PythonInt_Returns_Int32()
+ {
+ var arr = np.array(new int[] { 1, 2, 3 });
+ var result = np.power(arr, 2);
+
+ result.dtype.Should().Be(np.int32);
+ result.GetAtIndex(0).Should().Be(1);
+ result.GetAtIndex(1).Should().Be(4);
+ result.GetAtIndex(2).Should().Be(9);
+ }
+
+ #endregion
+
+ #region 16. Comparison with NumPy 1.x Behavior (Documentation)
+
+ ///
+ /// Documents what CHANGED from NumPy 1.x to 2.x behavior.
+ ///
+ /// NumPy 1.x: uint8([1,2,3]) + 5 → int64 (scalar widened to int64)
+ /// NumPy 2.x: uint8([1,2,3]) + 5 → uint8 (array dtype wins)
+ ///
+ /// NumSharp now follows NumPy 2.x behavior.
+ ///
+ [Test]
+ public void Documentation_NEP50_BreakingChange()
+ {
+ // This is the key behavioral change from NumPy 1.x to 2.x
+ var arr = np.array(new byte[] { 1, 2, 3 });
+ var result = arr + 5;
+
+ // NumPy 1.x would return int64
+ // NumPy 2.x (and NumSharp) returns uint8
+ result.dtype.Should().Be(np.uint8,
+ "NEP 50: array dtype wins when scalar is same-kind (integer)");
+ }
+
+ #endregion
+
+ #region 17. Cross-Kind Promotion (float wins over int)
+
+ ///
+ /// When kinds differ (int vs float), the higher kind wins.
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.int16) + 5.0).dtype)"
+ /// Output: float64
+ ///
+ /// Note: NumSharp doesn't support int8 (sbyte), so we use int16.
+ ///
+ [Test]
+ public void CrossKind_IntArray_Plus_Float_Returns_Float64()
+ {
+ var intArr = np.array(new short[] { 1, 2, 3 }); // int16
+ var result = intArr + 5.0;
+
+ result.dtype.Should().Be(np.float64, "Cross-kind: float wins over int");
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.uint8) + 5.0).dtype)"
+ /// Output: float64
+ ///
+ [Test]
+ public void CrossKind_UInt8Array_Plus_Float_Returns_Float64()
+ {
+ var arr = np.array(new byte[] { 1, 2, 3 });
+ var result = arr + 5.0;
+
+ result.dtype.Should().Be(np.float64, "Cross-kind: float wins over uint");
+ }
+
+ ///
+ /// Verified: python3 -c "import numpy as np; print((np.array([1,2,3], np.int32) + 5.0).dtype)"
+ /// Output: float64
+ ///
+ [Test]
+ public void CrossKind_Int32Array_Plus_Float_Returns_Float64()
+ {
+ var arr = np.array(new int[] { 1, 2, 3 });
+ var result = arr + 5.0;
+
+ result.dtype.Should().Be(np.float64, "Cross-kind: float wins over int");
+ }
+
+ #endregion
+
+ #region 18. Complete NEP50 Operation Matrix (12 combinations × 4 operations)
+
+ // ============================================================================
+ // UINT8 + SIGNED SCALARS (3 combinations)
+ // ============================================================================
+
+ ///
+ /// Verified: python3 -c "import numpy as np; a=np.array([1,2,3], np.uint8); print((a + np.int16(5)).dtype)"
+ /// NumPy 2.x with numpy scalar: int16 (strongly typed)
+ /// NumSharp with C# short: treats as weakly typed → uint8
+ ///
+ [Test]
+ public void NEP50_UInt8_Plus_Short_AllOps()
+ {
+ var arr = np.array(new byte[] { 10, 20, 30 });
+ short scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint8, "uint8 + int16 → uint8");
+ (arr - scalar).dtype.Should().Be(np.uint8, "uint8 - int16 → uint8");
+ (arr * scalar).dtype.Should().Be(np.uint8, "uint8 * int16 → uint8");
+ (arr % scalar).dtype.Should().Be(np.uint8, "uint8 % int16 → uint8");
+ }
+
+ [Test]
+ public void NEP50_UInt8_Plus_Int_AllOps()
+ {
+ var arr = np.array(new byte[] { 10, 20, 30 });
+ int scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint8, "uint8 + int32 → uint8");
+ (arr - scalar).dtype.Should().Be(np.uint8, "uint8 - int32 → uint8");
+ (arr * scalar).dtype.Should().Be(np.uint8, "uint8 * int32 → uint8");
+ (arr % scalar).dtype.Should().Be(np.uint8, "uint8 % int32 → uint8");
+ }
+
+ [Test]
+ public void NEP50_UInt8_Plus_Long_AllOps()
+ {
+ var arr = np.array(new byte[] { 10, 20, 30 });
+ long scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint8, "uint8 + int64 → uint8");
+ (arr - scalar).dtype.Should().Be(np.uint8, "uint8 - int64 → uint8");
+ (arr * scalar).dtype.Should().Be(np.uint8, "uint8 * int64 → uint8");
+ (arr % scalar).dtype.Should().Be(np.uint8, "uint8 % int64 → uint8");
+ }
+
+ // ============================================================================
+ // UINT16 + SIGNED SCALARS (3 combinations)
+ // ============================================================================
+
+ [Test]
+ public void NEP50_UInt16_Plus_Short_AllOps()
+ {
+ var arr = np.array(new ushort[] { 100, 200, 300 });
+ short scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint16, "uint16 + int16 → uint16");
+ (arr - scalar).dtype.Should().Be(np.uint16, "uint16 - int16 → uint16");
+ (arr * scalar).dtype.Should().Be(np.uint16, "uint16 * int16 → uint16");
+ (arr % scalar).dtype.Should().Be(np.uint16, "uint16 % int16 → uint16");
+ }
+
+ [Test]
+ public void NEP50_UInt16_Plus_Int_AllOps()
+ {
+ var arr = np.array(new ushort[] { 100, 200, 300 });
+ int scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint16, "uint16 + int32 → uint16");
+ (arr - scalar).dtype.Should().Be(np.uint16, "uint16 - int32 → uint16");
+ (arr * scalar).dtype.Should().Be(np.uint16, "uint16 * int32 → uint16");
+ (arr % scalar).dtype.Should().Be(np.uint16, "uint16 % int32 → uint16");
+ }
+
+ [Test]
+ public void NEP50_UInt16_Plus_Long_AllOps()
+ {
+ var arr = np.array(new ushort[] { 100, 200, 300 });
+ long scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint16, "uint16 + int64 → uint16");
+ (arr - scalar).dtype.Should().Be(np.uint16, "uint16 - int64 → uint16");
+ (arr * scalar).dtype.Should().Be(np.uint16, "uint16 * int64 → uint16");
+ (arr % scalar).dtype.Should().Be(np.uint16, "uint16 % int64 → uint16");
+ }
+
+ // ============================================================================
+ // UINT32 + SIGNED SCALARS (3 combinations)
+ // ============================================================================
+
+ [Test]
+ public void NEP50_UInt32_Plus_Short_AllOps()
+ {
+ var arr = np.array(new uint[] { 1000, 2000, 3000 });
+ short scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint32, "uint32 + int16 → uint32");
+ (arr - scalar).dtype.Should().Be(np.uint32, "uint32 - int16 → uint32");
+ (arr * scalar).dtype.Should().Be(np.uint32, "uint32 * int16 → uint32");
+ (arr % scalar).dtype.Should().Be(np.uint32, "uint32 % int16 → uint32");
+ }
+
+ [Test]
+ public void NEP50_UInt32_Plus_Int_AllOps()
+ {
+ var arr = np.array(new uint[] { 1000, 2000, 3000 });
+ int scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint32, "uint32 + int32 → uint32");
+ (arr - scalar).dtype.Should().Be(np.uint32, "uint32 - int32 → uint32");
+ (arr * scalar).dtype.Should().Be(np.uint32, "uint32 * int32 → uint32");
+ (arr % scalar).dtype.Should().Be(np.uint32, "uint32 % int32 → uint32");
+ }
+
+ [Test]
+ public void NEP50_UInt32_Plus_Long_AllOps()
+ {
+ var arr = np.array(new uint[] { 1000, 2000, 3000 });
+ long scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint32, "uint32 + int64 → uint32");
+ (arr - scalar).dtype.Should().Be(np.uint32, "uint32 - int64 → uint32");
+ (arr * scalar).dtype.Should().Be(np.uint32, "uint32 * int64 → uint32");
+ (arr % scalar).dtype.Should().Be(np.uint32, "uint32 % int64 → uint32");
+ }
+
+ // ============================================================================
+ // UINT64 + SIGNED SCALARS (3 combinations)
+ // ============================================================================
+
+ [Test]
+ public void NEP50_UInt64_Plus_Short_AllOps()
+ {
+ var arr = np.array(new ulong[] { 10000, 20000, 30000 });
+ short scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint64, "uint64 + int16 → uint64");
+ (arr - scalar).dtype.Should().Be(np.uint64, "uint64 - int16 → uint64");
+ (arr * scalar).dtype.Should().Be(np.uint64, "uint64 * int16 → uint64");
+ (arr % scalar).dtype.Should().Be(np.uint64, "uint64 % int16 → uint64");
+ }
+
+ [Test]
+ public void NEP50_UInt64_Plus_Int_AllOps()
+ {
+ var arr = np.array(new ulong[] { 10000, 20000, 30000 });
+ int scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint64, "uint64 + int32 → uint64");
+ (arr - scalar).dtype.Should().Be(np.uint64, "uint64 - int32 → uint64");
+ (arr * scalar).dtype.Should().Be(np.uint64, "uint64 * int32 → uint64");
+ (arr % scalar).dtype.Should().Be(np.uint64, "uint64 % int32 → uint64");
+ }
+
+ [Test]
+ public void NEP50_UInt64_Plus_Long_AllOps()
+ {
+ var arr = np.array(new ulong[] { 10000, 20000, 30000 });
+ long scalar = 5;
+
+ (arr + scalar).dtype.Should().Be(np.uint64, "uint64 + int64 → uint64");
+ (arr - scalar).dtype.Should().Be(np.uint64, "uint64 - int64 → uint64");
+ (arr * scalar).dtype.Should().Be(np.uint64, "uint64 * int64 → uint64");
+ (arr % scalar).dtype.Should().Be(np.uint64, "uint64 % int64 → uint64");
+ }
+
+ #endregion
+
+ #region 19. Value Correctness Tests
+
+ ///
+ /// Verify actual computed values are correct, not just dtypes.
+ ///
+ [Test]
+ public void NEP50_Values_UInt8_Operations()
+ {
+ var arr = np.array(new byte[] { 10, 20, 30 });
+
+ var add = arr + 5;
+ add.GetAtIndex(0).Should().Be(15);
+ add.GetAtIndex(1).Should().Be(25);
+ add.GetAtIndex(2).Should().Be(35);
+
+ var sub = arr - 5;
+ sub.GetAtIndex(0).Should().Be(5);
+ sub.GetAtIndex(1).Should().Be(15);
+ sub.GetAtIndex(2).Should().Be(25);
+
+ var mul = arr * 2;
+ mul.GetAtIndex(0).Should().Be(20);
+ mul.GetAtIndex(1).Should().Be(40);
+ mul.GetAtIndex(2).Should().Be(60);
+
+ var mod = arr % 7;
+ mod.GetAtIndex(0).Should().Be(3); // 10 % 7
+ mod.GetAtIndex(1).Should().Be(6); // 20 % 7
+ mod.GetAtIndex(2).Should().Be(2); // 30 % 7
+ }
+
+ [Test]
+ public void NEP50_Values_UInt32_Operations()
+ {
+ var arr = np.array(new uint[] { 1000, 2000, 3000 });
+
+ var add = arr + 500;
+ add.GetAtIndex(0).Should().Be(1500);
+ add.GetAtIndex(1).Should().Be(2500);
+ add.GetAtIndex(2).Should().Be(3500);
+
+ var sub = arr - 500;
+ sub.GetAtIndex(0).Should().Be(500);
+ sub.GetAtIndex(1).Should().Be(1500);
+ sub.GetAtIndex(2).Should().Be(2500);
+ }
+
+ [Test]
+ public void NEP50_Values_UInt64_Operations()
+ {
+ var arr = np.array(new ulong[] { 10000, 20000, 30000 });
+
+ var add = arr + 5000L;
+ add.GetAtIndex(0).Should().Be(15000);
+ add.GetAtIndex(1).Should().Be(25000);
+ add.GetAtIndex(2).Should().Be(35000);
+ }
+
+ #endregion
+}
diff --git a/test/NumSharp.UnitTest/Logic/np.find_common_type.Test.cs b/test/NumSharp.UnitTest/Logic/np.find_common_type.Test.cs
index 27b7e303..693304b3 100644
--- a/test/NumSharp.UnitTest/Logic/np.find_common_type.Test.cs
+++ b/test/NumSharp.UnitTest/Logic/np.find_common_type.Test.cs
@@ -162,6 +162,35 @@ public void Case23()
r.Should().Be(NPTypeCode.Int16);
}
+ ///
+ /// NEP 50: When an unsigned integer array operates with a signed integer scalar,
+ /// the array dtype wins (no type widening). This matches NumPy 2.x behavior.
+ /// See: https://numpy.org/neps/nep-0050-scalar-promotion.html
+ ///
+ [Test]
+ public void NEP50_UnsignedArray_SignedScalar_ArrayWins()
+ {
+ // uint8 array + signed scalar → uint8
+ np._FindCommonArrayScalarType(NPTypeCode.Byte, NPTypeCode.Int16).Should().Be(NPTypeCode.Byte);
+ np._FindCommonArrayScalarType(NPTypeCode.Byte, NPTypeCode.Int32).Should().Be(NPTypeCode.Byte);
+ np._FindCommonArrayScalarType(NPTypeCode.Byte, NPTypeCode.Int64).Should().Be(NPTypeCode.Byte);
+
+ // uint16 array + signed scalar → uint16
+ np._FindCommonArrayScalarType(NPTypeCode.UInt16, NPTypeCode.Int16).Should().Be(NPTypeCode.UInt16);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt16, NPTypeCode.Int32).Should().Be(NPTypeCode.UInt16);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt16, NPTypeCode.Int64).Should().Be(NPTypeCode.UInt16);
+
+ // uint32 array + signed scalar → uint32
+ np._FindCommonArrayScalarType(NPTypeCode.UInt32, NPTypeCode.Int16).Should().Be(NPTypeCode.UInt32);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt32, NPTypeCode.Int32).Should().Be(NPTypeCode.UInt32);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt32, NPTypeCode.Int64).Should().Be(NPTypeCode.UInt32);
+
+ // uint64 array + signed scalar → uint64
+ np._FindCommonArrayScalarType(NPTypeCode.UInt64, NPTypeCode.Int16).Should().Be(NPTypeCode.UInt64);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt64, NPTypeCode.Int32).Should().Be(NPTypeCode.UInt64);
+ np._FindCommonArrayScalarType(NPTypeCode.UInt64, NPTypeCode.Int64).Should().Be(NPTypeCode.UInt64);
+ }
+
[Test, Skip("Ignored")]
public void gen_typecode_map()
{