// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Tests.Common; public class TensorPrimitivesTests { /// /// Gets lengths that exercise scalar execution, every SIMD width, overlapping tails, and the unrolled loop. /// public static TheoryData SpanLengths => new() { 0, 1, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 2048 }; /// /// Verifies that byte addition wraps modulo 256 and supports either input as the in-place destination. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void AddByteMatchesScalarFormula(int length) { byte[] x = new byte[length]; byte[] y = new byte[length]; byte[] expected = new byte[length]; for (int i = 0; i < length; i++) { x[i] = (byte)((i * 23) + 197); y[i] = (byte)((i * 41) + 113); expected[i] = unchecked((byte)(x[i] + y[i])); } byte[] destination = new byte[length]; TensorPrimitives_.Add(x, y, destination); Assert.Equal(expected, destination); byte[] xInPlace = (byte[])x.Clone(); TensorPrimitives_.Add(xInPlace, y, xInPlace); Assert.Equal(expected, xInPlace); byte[] yInPlace = (byte[])y.Clone(); TensorPrimitives_.Add(x, yInPlace, yInPlace); Assert.Equal(expected, yInPlace); } /// /// Verifies that unsigned integer addition preserves unchecked histogram accumulation semantics. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void AddUInt32MatchesScalarFormula(int length) { uint[] x = new uint[length]; uint[] y = new uint[length]; uint[] expected = new uint[length]; for (int i = 0; i < length; i++) { x[i] = ((uint)i * 1_234_567U) + 0xF0000000U; y[i] = ((uint)i * 7_654_321U) + 0x30000000U; expected[i] = unchecked(x[i] + y[i]); } TensorPrimitives_.Add(x, y, x); Assert.Equal(expected, x); } /// /// Verifies that scalar integer addition produces identical results for separate and in-place destinations. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void AddScalarInt32MatchesScalarFormula(int length) { int[] source = new int[length]; int[] expected = new int[length]; const int addend = 17; for (int i = 0; i < length; i++) { source[i] = (i * 37) - 200; expected[i] = source[i] + addend; } int[] destination = new int[length]; TensorPrimitives_.Add(source, addend, destination); Assert.Equal(expected, destination); int[] inPlace = (int[])source.Clone(); TensorPrimitives_.Add(inPlace, addend, inPlace); Assert.Equal(expected, inPlace); } /// /// Verifies that floating-point negation preserves the scalar operator's exact bit-level behavior. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void NegateSingleMatchesScalarFormula(int length) { float[] values = { float.NaN, -0F, 0F, -1F, 1F, float.NegativeInfinity, float.PositiveInfinity }; float[] source = new float[length]; float[] expected = new float[length]; for (int i = 0; i < source.Length; i++) { source[i] = values[i % values.Length]; expected[i] = -source[i]; } float[] destination = new float[length]; TensorPrimitives_.Negate(source, destination); AssertSingleBitsEqual(expected, destination); TensorPrimitives_.Negate(source, source); AssertSingleBitsEqual(expected, source); } /// /// Verifies that integer clamping produces identical results for separate and in-place destinations. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void ClampInt32MatchesScalarFormula(int length) { int[] source = new int[length]; int[] expected = new int[length]; for (int i = 0; i < source.Length; i++) { source[i] = ((i * 37) % 401) - 200; expected[i] = Math.Clamp(source[i], -73, 91); } int[] destination = new int[length]; TensorPrimitives_.Clamp(source, -73, 91, destination); Assert.Equal(expected, destination); int[] inPlace = (int[])source.Clone(); TensorPrimitives_.Clamp(inPlace, -73, 91, inPlace); Assert.Equal(expected, inPlace); } /// /// Verifies that floating-point clamping matches the runtime tensor formula for special values and unordered bounds. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void ClampSingleMatchesRuntimeFormula(int length) { float[] values = { float.NaN, -0F, 0F, -1F, 1F, float.NegativeInfinity, float.PositiveInfinity }; float[] source = new float[length]; float[] expected = new float[length]; for (int i = 0; i < source.Length; i++) { source[i] = values[i % values.Length]; // Runtime main follows Min(Max(x, min), max) for vectorizable types, including unordered bounds. expected[i] = float.Min(float.Max(source[i], 2F), -2F); } TensorPrimitives_.Clamp(source, 2F, -2F, source); AssertSingleBitsEqual(expected, source); } /// /// Verifies that single-precision clamping preserves the runtime's signed-zero and NaN behavior. /// [Fact] public void ClampSinglePreservesRuntimeSpecialValueSemantics() { float[] values = { float.NaN, float.NegativeInfinity, -0F, 0F, float.PositiveInfinity }; float[] actual = new float[129]; float[] expected = new float[actual.Length]; for (int i = 0; i < actual.Length; i++) { actual[i] = values[i % values.Length]; expected[i] = float.Min(float.Max(actual[i], -0F), 0F); } TensorPrimitives_.Clamp(actual, -0F, 0F, actual); AssertSingleBitsEqual(expected, actual); } /// /// Verifies that double-precision clamping preserves the runtime's signed-zero and NaN behavior. /// [Fact] public void ClampDoublePreservesRuntimeSpecialValueSemantics() { double[] values = { double.NaN, double.NegativeInfinity, -0D, 0D, double.PositiveInfinity }; double[] actual = new double[65]; double[] expected = new double[actual.Length]; for (int i = 0; i < actual.Length; i++) { actual[i] = values[i % values.Length]; expected[i] = double.Min(double.Max(actual[i], -0D), 0D); } TensorPrimitives_.Clamp(actual, -0D, 0D, actual); AssertDoubleBitsEqual(expected, actual); } /// /// Verifies that division produces identical results for separate and in-place destinations. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void DivideSingleMatchesScalarFormula(int length) { float[] source = new float[length]; float[] expected = new float[length]; for (int i = 0; i < source.Length; i++) { source[i] = (i - 65.25F) * 1.75F; expected[i] = source[i] / 3.25F; } float[] destination = new float[length]; TensorPrimitives_.Divide(source, 3.25F, destination); AssertSingleBitsEqual(expected, destination); float[] inPlace = (float[])source.Clone(); TensorPrimitives_.Divide(inPlace, 3.25F, inPlace); AssertSingleBitsEqual(expected, inPlace); } /// /// Verifies that maximum selection preserves the runtime's NaN and signed-zero semantics. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void MaxSingleMatchesRuntimeFormula(int length) { float[] values = { float.NaN, float.NegativeInfinity, -1F, -0F, 0F, 1F, float.PositiveInfinity }; float[] actual = new float[length]; float[] expected = new float[length]; for (int i = 0; i < length; i++) { actual[i] = values[i % values.Length]; expected[i] = float.Max(actual[i], -0F); } TensorPrimitives_.Max(actual, -0F, actual); AssertSingleBitsEqual(expected, actual); } /// /// Verifies that multiplication produces identical results for separate and in-place destinations. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void MultiplySingleMatchesScalarFormula(int length) { float[] source = new float[length]; float[] expected = new float[length]; for (int i = 0; i < source.Length; i++) { source[i] = (i - 65.25F) * 1.75F; expected[i] = source[i] * 0.375F; } float[] destination = new float[length]; TensorPrimitives_.Multiply(source, 0.375F, destination); AssertSingleBitsEqual(expected, destination); TensorPrimitives_.Multiply(source, 0.375F, source); AssertSingleBitsEqual(expected, source); } /// /// Verifies that the normalization compatibility call preserves its element-wise division contract. /// /// The input length. [Theory] [MemberData(nameof(SpanLengths))] public void NormalizeMatchesScalarFormula(int length) { float[] actual = new float[length]; float[] expected = new float[length]; for (int i = 0; i < actual.Length; i++) { actual[i] = (i + 1) * 0.125F; expected[i] = actual[i] / 7.5F; } Numerics.Normalize(actual, 7.5F); AssertSingleBitsEqual(expected, actual); } /// /// Compares floating-point results while preserving signed-zero behavior. /// /// The expected values. /// The actual values. private static void AssertSingleBitsEqual(ReadOnlySpan expected, ReadOnlySpan actual) { Assert.Equal(expected.Length, actual.Length); for (int i = 0; i < expected.Length; i++) { if (float.IsNaN(expected[i])) { Assert.True(float.IsNaN(actual[i])); } else { Assert.Equal(BitConverter.SingleToInt32Bits(expected[i]), BitConverter.SingleToInt32Bits(actual[i])); } } } /// /// Compares double-precision results while preserving signed-zero behavior. /// /// The expected values. /// The actual values. private static void AssertDoubleBitsEqual(ReadOnlySpan expected, ReadOnlySpan actual) { Assert.Equal(expected.Length, actual.Length); for (int i = 0; i < expected.Length; i++) { if (double.IsNaN(expected[i])) { Assert.True(double.IsNaN(actual[i])); } else { Assert.Equal(BitConverter.DoubleToInt64Bits(expected[i]), BitConverter.DoubleToInt64Bits(actual[i])); } } } }