From edd77d6bf0ac567f3697e2560e54e9901113654e Mon Sep 17 00:00:00 2001 From: Linus Hamlin Date: Fri, 22 May 2026 15:06:09 +0200 Subject: [PATCH 1/2] Add unit tests --- .../tests/TensorPrimitives.Generic.cs | 3 +- .../tests/TensorPrimitivesTests.cs | 113 +++++++++++++++--- 2 files changed, 97 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs index f9ed5e03bb1869..433fcef4faf7b7 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs @@ -2725,7 +2725,8 @@ public unsafe abstract class GenericNumberTensorPrimitivesTests : TensorPrimi protected override T SumOfSquares(ReadOnlySpan x) => TensorPrimitives.SumOfSquares(x); protected override T ConvertFromSingle(float f) => T.CreateTruncating(f); - protected override bool IsFloatingPoint => typeof(T) == typeof(Half) || base.IsFloatingPoint; + protected override bool IsFloatingPoint => typeof(T) == typeof(NFloat) || typeof(T) == typeof(Half) || base.IsFloatingPoint; + protected override bool IsUnsignedInteger => typeof(T) == typeof(UInt128) || typeof(T) == typeof(nuint) || base.IsUnsignedInteger; protected override T NextRandom() { diff --git a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitivesTests.cs b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitivesTests.cs index 55dc2be94487d9..f428af07f79d64 100644 --- a/src/libraries/System.Numerics.Tensors/tests/TensorPrimitivesTests.cs +++ b/src/libraries/System.Numerics.Tensors/tests/TensorPrimitivesTests.cs @@ -105,6 +105,9 @@ public abstract class TensorPrimitivesTests where T : unmanaged, IEquatable x); protected virtual bool IsFloatingPoint => typeof(T) == typeof(float) || typeof(T) == typeof(double); + protected virtual bool IsUnsignedInteger => + typeof(T) == typeof(byte) || typeof(T) == typeof(ushort) || typeof(T) == typeof(uint) || + typeof(T) == typeof(ulong) || typeof(T) == typeof(char); protected virtual int? IndexOfSizeExceedingMaxValue() => (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte)) ? Helpers.SizeGreaterThanByte : @@ -1138,6 +1141,18 @@ public void IndexOfMax_Negative0LesserThanPositive0() Assert.Equal(0, IndexOfMax([ConvertFromSingle(+0f), ConvertFromSingle(-0f)])); Assert.Equal(1, IndexOfMax([ConvertFromSingle(-1), ConvertFromSingle(-0f)])); Assert.Equal(2, IndexOfMax([ConvertFromSingle(-1), ConvertFromSingle(-0f), ConvertFromSingle(1f)])); + + Assert.All(Helpers.TensorLengths, tensorLength => + { + foreach (int expected in new[] { 0, tensorLength / 2, tensorLength - 1 }) + { + using BoundedMemory x = CreateTensor(tensorLength); + x.Span.Fill(NegativeZero); + x[expected] = Zero; + x[tensorLength - 1] = Zero; + Assert.Equal(expected, IndexOfMax(x.Span)); + } + }); } [Fact] @@ -1167,17 +1182,12 @@ public void IndexOfMaxMagnitude_AllLengths() { foreach (int expected in new[] { 0, tensorLength / 2, tensorLength - 1 }) { - using BoundedMemory x = CreateTensor(tensorLength); - FillTensor(x, MinValue); + using BoundedMemory x = CreateAndFillTensor(tensorLength); T max = x[0]; for (int i = 0; i < x.Length; i++) { - int compared = Comparer.Default.Compare(Abs(x[i]), Abs(max)); - if (compared > 0 || (compared == 0 && EqualityComparer.Default.Equals(x[i], max))) - { - max = x[i]; - } + max = MaxMagnitude(max, x[i]); } x[expected] = max; @@ -1188,11 +1198,11 @@ public void IndexOfMaxMagnitude_AllLengths() Assert.True(actual < expected || Comparer.Default.Compare(x[actual], x[expected]) > 0, $"{tensorLength} {actual} {expected} {string.Join(",", MemoryMarshal.ToEnumerable(x.Memory))}"); if (IsFloatingPoint) { - AssertEqualTolerance(Abs(x[expected]), Abs(x[actual])); + AssertEqualTolerance(x[expected], x[actual], Zero); } else { - Assert.Equal(Abs(x[expected]), Abs(x[actual])); + Assert.Equal(x[expected], x[actual]); } } } @@ -1216,6 +1226,24 @@ public void IndexOfMaxMagnitude_FirstNaNReturned() }); } + [Fact] + public void IndexOfMaxMagnitude_Negative1LesserThanPositive1() + { + if (IsUnsignedInteger) return; + + Assert.All(Helpers.TensorLengths, tensorLength => + { + foreach (int expected in new[] { 0, tensorLength / 2, tensorLength - 1 }) + { + using BoundedMemory x = CreateTensor(tensorLength); + x.Span.Fill(NegativeOne); + x[expected] = One; + x[tensorLength - 1] = One; + Assert.Equal(expected, IndexOfMaxMagnitude(x.Span)); + } + }); + } + [Fact] public void IndexOfMaxMagnitude_Negative0LesserThanPositive0() { @@ -1227,6 +1255,18 @@ public void IndexOfMaxMagnitude_Negative0LesserThanPositive0() Assert.Equal(0, IndexOfMaxMagnitude([ConvertFromSingle(+0f), ConvertFromSingle(-0f)])); Assert.Equal(0, IndexOfMaxMagnitude([ConvertFromSingle(-1), ConvertFromSingle(-0f)])); Assert.Equal(2, IndexOfMaxMagnitude([ConvertFromSingle(-1), ConvertFromSingle(-0f), ConvertFromSingle(1f)])); + + Assert.All(Helpers.TensorLengths, tensorLength => + { + foreach (int expected in new[] { 0, tensorLength / 2, tensorLength - 1 }) + { + using BoundedMemory x = CreateTensor(tensorLength); + x.Span.Fill(NegativeZero); + x[expected] = Zero; + x[tensorLength - 1] = Zero; + Assert.Equal(expected, IndexOfMaxMagnitude(x.Span)); + } + }); } [Fact] @@ -1291,6 +1331,18 @@ public void IndexOfMin_Negative0LesserThanPositive0() Assert.Equal(1, IndexOfMin([ConvertFromSingle(+0f), ConvertFromSingle(-0f), ConvertFromSingle(-0f), ConvertFromSingle(-0f), ConvertFromSingle(-0f)])); Assert.Equal(0, IndexOfMin([ConvertFromSingle(-1), ConvertFromSingle(-0f)])); Assert.Equal(0, IndexOfMin([ConvertFromSingle(-1), ConvertFromSingle(-0f), ConvertFromSingle(1f)])); + + Assert.All(Helpers.TensorLengths, tensorLength => + { + foreach (int expected in new[] { 0, tensorLength / 2, tensorLength - 1 }) + { + using BoundedMemory x = CreateTensor(tensorLength); + x.Span.Fill(Zero); + x[expected] = NegativeZero; + x[tensorLength - 1] = NegativeZero; + Assert.Equal(expected, IndexOfMin(x.Span)); + } + }); } [Fact] @@ -1320,17 +1372,12 @@ public void IndexOfMinMagnitude_AllLengths() { foreach (int expected in new[] { 0, tensorLength / 2, tensorLength - 1 }) { - using BoundedMemory x = CreateTensor(tensorLength); - FillTensor(x, MinValue); + using BoundedMemory x = CreateAndFillTensor(tensorLength); T min = x[0]; for (int i = 0; i < x.Length; i++) { - int compared = Comparer.Default.Compare(Abs(x[i]), Abs(min)); - if (compared < 0 || (compared == 0 && Comparer.Default.Compare(x[i], min) < 0)) - { - min = x[i]; - } + min = MinMagnitude(min, x[i]); } x[expected] = min; @@ -1341,11 +1388,11 @@ public void IndexOfMinMagnitude_AllLengths() Assert.True(actual < expected || Comparer.Default.Compare(x[actual], x[expected]) < 0, $"{tensorLength} {actual} {expected} {string.Join(",", MemoryMarshal.ToEnumerable(x.Memory))}"); if (IsFloatingPoint) { - AssertEqualTolerance(Abs(x[expected]), Abs(x[actual])); + AssertEqualTolerance(x[expected], x[actual], Zero); } else { - Assert.Equal(Abs(x[expected]), Abs(x[actual])); + Assert.Equal(x[expected], x[actual]); } } } @@ -1369,6 +1416,24 @@ public void IndexOfMinMagnitude_FirstNaNReturned() }); } + [Fact] + public void IndexOfMinMagnitude_Negative1LesserThanPositive1() + { + if (IsUnsignedInteger) return; + + Assert.All(Helpers.TensorLengths, tensorLength => + { + foreach (int expected in new[] { 0, tensorLength / 2, tensorLength - 1 }) + { + using BoundedMemory x = CreateTensor(tensorLength); + x.Span.Fill(One); + x[expected] = NegativeOne; + x[tensorLength - 1] = NegativeOne; + Assert.Equal(expected, IndexOfMinMagnitude(x.Span)); + } + }); + } + [Fact] public void IndexOfMinMagnitude_Negative0LesserThanPositive0() { @@ -1380,6 +1445,18 @@ public void IndexOfMinMagnitude_Negative0LesserThanPositive0() Assert.Equal(1, IndexOfMinMagnitude([ConvertFromSingle(+0f), ConvertFromSingle(-0f), ConvertFromSingle(-0f), ConvertFromSingle(-0f)])); Assert.Equal(1, IndexOfMinMagnitude([ConvertFromSingle(-1), ConvertFromSingle(-0f)])); Assert.Equal(1, IndexOfMinMagnitude([ConvertFromSingle(-1), ConvertFromSingle(-0f), ConvertFromSingle(1f)])); + + Assert.All(Helpers.TensorLengths, tensorLength => + { + foreach (int expected in new[] { 0, tensorLength / 2, tensorLength - 1 }) + { + using BoundedMemory x = CreateTensor(tensorLength); + x.Span.Fill(Zero); + x[expected] = NegativeZero; + x[tensorLength - 1] = NegativeZero; + Assert.Equal(expected, IndexOfMinMagnitude(x.Span)); + } + }); } [Fact] From cac8cea81fc385c88379ec7a1cb4d0de02a97861 Mon Sep 17 00:00:00 2001 From: Linus Hamlin Date: Fri, 22 May 2026 15:52:00 +0200 Subject: [PATCH 2/2] Fix IndexOfMin/MaxMagnitude --- .../netcore/TensorPrimitives.IndexOfMaxMagnitude.cs | 12 +++++++++--- .../netcore/TensorPrimitives.IndexOfMinMagnitude.cs | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.IndexOfMaxMagnitude.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.IndexOfMaxMagnitude.cs index 5ca77310c5fa3d..abc71d59b9cb52 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.IndexOfMaxMagnitude.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.IndexOfMaxMagnitude.cs @@ -72,7 +72,9 @@ public static Vector128 Compare(Vector128 x, Vector128 y) || typeof(T) == typeof(nint)) { // Consider overflows (when IsNegative(Abs(x))) from Abs(MinValue) which implies maximum magnitude. - return Vector128.AndNot(Vector128.GreaterThan(xMag, yMag) | IsNegative(xMag), IsNegative(yMag)); + Vector128 equalResult = Vector128.IsPositive(x) & Vector128.IsNegative(y); + Vector128 nonOverflowResult = Vector128.GreaterThan(xMag, yMag) | (Vector128.Equals(xMag, yMag) & equalResult); + return Vector128.AndNot(nonOverflowResult | IsNegative(xMag), IsNegative(yMag)); } else { @@ -96,7 +98,9 @@ public static Vector256 Compare(Vector256 x, Vector256 y) || typeof(T) == typeof(nint)) { // Consider overflows (when IsNegative(Abs(x))) from Abs(MinValue) which implies maximum magnitude. - return Vector256.AndNot(Vector256.GreaterThan(xMag, yMag) | IsNegative(xMag), IsNegative(yMag)); + Vector256 equalResult = Vector256.IsPositive(x) & Vector256.IsNegative(y); + Vector256 nonOverflowResult = Vector256.GreaterThan(xMag, yMag) | (Vector256.Equals(xMag, yMag) & equalResult); + return Vector256.AndNot(nonOverflowResult | Vector256.IsNegative(xMag), Vector256.IsNegative(yMag)); } else { @@ -120,7 +124,9 @@ public static Vector512 Compare(Vector512 x, Vector512 y) || typeof(T) == typeof(nint)) { // Consider overflows (when IsNegative(Abs(x))) from Abs(MinValue) which implies maximum magnitude. - return Vector512.AndNot(Vector512.GreaterThan(xMag, yMag) | IsNegative(xMag), IsNegative(yMag)); + Vector512 equalResult = Vector512.IsPositive(x) & Vector512.IsNegative(y); + Vector512 nonOverflowResult = Vector512.GreaterThan(xMag, yMag) | (Vector512.Equals(xMag, yMag) & equalResult); + return Vector512.AndNot(nonOverflowResult | Vector512.IsNegative(xMag), Vector512.IsNegative(yMag)); } else { diff --git a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.IndexOfMinMagnitude.cs b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.IndexOfMinMagnitude.cs index 437c9537e6962e..1ed73fac95a636 100644 --- a/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.IndexOfMinMagnitude.cs +++ b/src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.IndexOfMinMagnitude.cs @@ -72,7 +72,9 @@ public static Vector128 Compare(Vector128 x, Vector128 y) || typeof(T) == typeof(nint)) { // Consider overflows (when IsNegative(Abs(x))) from Abs(MinValue) which implies maximum magnitude. - return Vector128.AndNot(Vector128.LessThan(xMag, yMag) | IsNegative(yMag), IsNegative(xMag)); + Vector128 equalResult = Vector128.IsNegative(x) & Vector128.IsPositive(y); + Vector128 nonOverflowResult = Vector128.LessThan(xMag, yMag) | (Vector128.Equals(xMag, yMag) & equalResult); + return Vector128.AndNot(nonOverflowResult | Vector128.IsNegative(yMag), Vector128.IsNegative(xMag)); } else { @@ -96,7 +98,9 @@ public static Vector256 Compare(Vector256 x, Vector256 y) || typeof(T) == typeof(nint)) { // Consider overflows (when IsNegative(Abs(x))) from Abs(MinValue) which implies maximum magnitude. - return Vector256.AndNot(Vector256.LessThan(xMag, yMag) | IsNegative(yMag), IsNegative(xMag)); + Vector256 equalResult = Vector256.IsNegative(x) & Vector256.IsPositive(y); + Vector256 nonOverflowResult = Vector256.LessThan(xMag, yMag) | (Vector256.Equals(xMag, yMag) & equalResult); + return Vector256.AndNot(nonOverflowResult | Vector256.IsNegative(yMag), Vector256.IsNegative(xMag)); } else { @@ -120,7 +124,9 @@ public static Vector512 Compare(Vector512 x, Vector512 y) || typeof(T) == typeof(nint)) { // Consider overflows (when IsNegative(Abs(x))) from Abs(MinValue) which implies maximum magnitude. - return Vector512.AndNot(Vector512.LessThan(xMag, yMag) | IsNegative(yMag), IsNegative(xMag)); + Vector512 equalResult = Vector512.IsNegative(x) & Vector512.IsPositive(y); + Vector512 nonOverflowResult = Vector512.LessThan(xMag, yMag) | (Vector512.Equals(xMag, yMag) & equalResult); + return Vector512.AndNot(nonOverflowResult | Vector512.IsNegative(yMag), Vector512.IsNegative(xMag)); } else {