Initial commit.

This commit is contained in:
Marek Lemańczyk committed 2025-09-09 21:15:57 +02:00
1 parent e5b0fea18c
commit 9f35f6c558
94 files changed
+12857

No files matched your search

@@ -0,0 +1,12 @@
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class AlphaCacheTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
}
@@ -0,0 +1,12 @@
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class AlphaCalculationsTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
}
@@ -0,0 +1,22 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class EulerPrimeTesterTests
{
private readonly EulerPrimeTester _tester = new(new PrimeTester());
[Theory]
[InlineData(13UL, true)]
[InlineData(29UL, true)]
[InlineData(3UL, false)]
[InlineData(21UL, false)]
[InlineData(1UL, false)]
public void IsEulerPrime_returns_expected(ulong n, bool expected)
{
_tester.IsEulerPrime(n).Should().Be(expected);
}
}
@@ -0,0 +1,69 @@
using FluentAssertions;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
public class GpuContextPoolTests
{
[Fact]
[Trait("Category", "Fast")]
public void RentPreferred_cpu_returns_cpu_accelerator()
{
GpuContextPool.DisposeAll();
using var lease = GpuContextPool.RentPreferred(preferCpu: true);
lease.Accelerator.AcceleratorType.Should().Be(AcceleratorType.CPU);
GpuContextPool.DisposeAll();
}
[Fact]
[Trait("Category", "Fast")]
public void RentPreferred_reuses_cpu_context()
{
GpuContextPool.DisposeAll();
Context firstContext;
Accelerator firstAccelerator;
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
firstContext = lease.Context;
firstAccelerator = lease.Accelerator;
}
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
lease.Context.Should().BeSameAs(firstContext);
lease.Accelerator.Should().BeSameAs(firstAccelerator);
}
GpuContextPool.DisposeAll();
}
[Fact]
[Trait("Category", "Fast")]
public void DisposeAll_clears_pooled_contexts()
{
GpuContextPool.DisposeAll();
Context firstContext;
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
firstContext = lease.Context;
}
GpuContextPool.DisposeAll();
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
lease.Context.Should().NotBeSameAs(firstContext);
}
GpuContextPool.DisposeAll();
}
}
@@ -0,0 +1,9 @@
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[CollectionDefinition("GpuNtt", DisableParallelization = true)]
public class GpuNttCollection
{
}
@@ -0,0 +1,45 @@
using System.Diagnostics;
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
[Trait("Category", "Fast")]
public class GpuPrimeLimiterTests
{
[Fact]
public async Task Acquire_blocks_when_limit_reached()
{
// Ensure limit = 1 so second acquire must wait.
GpuPrimeWorkLimiter.SetLimit(1);
var startedSecond = new TaskCompletionSource<bool>();
var sw = new Stopwatch();
using var first = GpuPrimeWorkLimiter.Acquire();
sw.Start();
var secondTask = Task.Run(() =>
{
using var second = GpuPrimeWorkLimiter.Acquire();
startedSecond.TrySetResult(true);
});
// Give secondTask a moment to attempt acquire (it should block).
await Task.Delay(50);
startedSecond.Task.IsCompleted.Should().BeFalse();
// Release first after short delay, allowing second to continue.
// (Dispose at scope end, but we want to end now.)
first.Dispose();
await secondTask.WaitAsync(TimeSpan.FromSeconds(1));
sw.Stop();
// Should be > ~50ms because second had to wait for first to release.
sw.ElapsedMilliseconds.Should().BeGreaterThanOrEqualTo(50);
}
}
@@ -0,0 +1,342 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
[Trait("Category", "Fast")]
public class GpuUInt128Tests
{
[Fact]
public void Mul_matches_UInt128()
{
var random = new Random(0);
for (var i = 0; i < 100; i++)
{
var high1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var high2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var a = new GpuUInt128(high1, low1);
var b = new GpuUInt128(high2, low2);
var expected = (UInt128)new GpuUInt128(high1, low1) * (UInt128)new GpuUInt128(high2, low2);
a = a.Mul(b);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void MulMod_matches_UInt128()
{
var random = new Random(1);
for (var i = 0; i < 100; i++)
{
var high1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var high2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus);
var b = new GpuUInt128((UInt128)new GpuUInt128(high2, low2) % (UInt128)modulus);
var expected = MulModReference((UInt128)a, (UInt128)b, (UInt128)modulus);
a = a.MulMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void MulMod_with_ulong_matches_UInt128()
{
var random = new Random(5);
for (var i = 0; i < 100; i++)
{
var high1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus);
ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
UInt128 bBig = (UInt128)bRaw % (UInt128)modulus;
ulong b = (ulong)bBig;
var expected = MulModReference((UInt128)a, bBig, (UInt128)modulus);
a = a.MulMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void MulMod_with_ulong_modulus_matches_UInt128()
{
var random = new Random(11);
for (var i = 0; i < 100; i++)
{
ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
ulong aRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong aVal = aRaw % modulus;
ulong bVal = bRaw % modulus;
var a = new GpuUInt128(aVal);
var b = new GpuUInt128(bVal);
UInt128 expected = ((UInt128)aVal * bVal) % modulus;
a = a.MulMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void MulModBigInteger_matches_UInt128()
{
var random = new Random(13);
for (var i = 0; i < 20; i++)
{
var high1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var high2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus);
var b = new GpuUInt128((UInt128)new GpuUInt128(high2, low2) % (UInt128)modulus);
var expected = MulModReference((UInt128)a, (UInt128)b, (UInt128)modulus);
var result = new GpuUInt128(a.High, a.Low);
result = result.MulModBigInteger(b, modulus);
((UInt128)result).Should().Be(expected);
}
}
[Fact]
public void SquareMod_matches_UInt128()
{
var random = new Random(8);
for (var i = 0; i < 100; i++)
{
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
var expected = MulModReference((UInt128)a, (UInt128)a, (UInt128)modulus);
a = a.SquareMod(modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void ModPow_matches_BigInteger()
{
var random = new Random(2);
for (var i = 0; i < 20; i++)
{
var baseHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var baseLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var expHigh = 0UL;
var expLow = (ulong)random.Next(1, int.MaxValue);
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var value = new GpuUInt128((UInt128)new GpuUInt128(baseHigh, baseLow) % (UInt128)modulus);
var exponent = new GpuUInt128(expHigh, expLow);
var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, (System.Numerics.BigInteger)(UInt128)exponent, (System.Numerics.BigInteger)(UInt128)modulus);
value = value.ModPow(exponent, modulus);
((UInt128)value).Should().Be(expected);
}
}
[Fact]
public void ModPow_with_ulong_matches_BigInteger()
{
var random = new Random(9);
for (var i = 0; i < 20; i++)
{
var baseHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var baseLow = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong exponent = (ulong)random.Next(1, int.MaxValue);
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var value = new GpuUInt128((UInt128)new GpuUInt128(baseHigh, baseLow) % (UInt128)modulus);
var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, exponent, (System.Numerics.BigInteger)(UInt128)modulus);
value = value.ModPow(exponent, modulus);
((UInt128)value).Should().Be(expected);
}
}
[Fact]
public void SubMod_matches_UInt128()
{
var random = new Random(3);
for (var i = 0; i < 100; i++)
{
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var subHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var subLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
var b = new GpuUInt128((UInt128)new GpuUInt128(subHigh, subLow) % (UInt128)modulus);
UInt128 expected = ((UInt128)a + (UInt128)modulus - (UInt128)b) % (UInt128)modulus;
a = a.SubMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void SubMod_with_ulong_matches_UInt128()
{
var random = new Random(6);
for (var i = 0; i < 100; i++)
{
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
ulong subRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
UInt128 bBig = (UInt128)subRaw % (UInt128)modulus;
ulong b = (ulong)bBig;
UInt128 expected = ((UInt128)a + (UInt128)modulus - bBig) % (UInt128)modulus;
a = a.SubMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void AddMod_matches_UInt128()
{
var random = new Random(4);
for (var i = 0; i < 100; i++)
{
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var addHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var addLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var a = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
var b = new GpuUInt128((UInt128)new GpuUInt128(addHigh, addLow) % (UInt128)modulus);
UInt128 expected = ((UInt128)a + (UInt128)b) % (UInt128)modulus;
a = a.AddMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void AddMod_with_ulong_modulus_matches_UInt128()
{
var random = new Random(12);
for (var i = 0; i < 100; i++)
{
ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
ulong aRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong aVal = aRaw % modulus;
ulong bVal = bRaw % modulus;
var a = new GpuUInt128(aVal);
var b = new GpuUInt128(bVal);
UInt128 expected = ((UInt128)aVal + bVal) % modulus;
a = a.AddMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void AddMod_with_ulong_operand_and_modulus_matches_UInt128()
{
var random = new Random(13);
for (var i = 0; i < 100; i++)
{
ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
ulong aRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong bRaw = ((ulong)random.Next() << 32) | (uint)random.Next();
ulong aVal = aRaw % modulus;
ulong bVal = bRaw % modulus;
var a = new GpuUInt128(aVal);
UInt128 expected = ((UInt128)aVal + bVal) % modulus;
a = a.AddMod(bVal, modulus);
((UInt128)a).Should().Be(expected);
}
}
[Fact]
public void ModInv_matches_BigInteger()
{
var random = new Random(7);
for (var i = 0; i < 20; i++)
{
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var valueHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var valueLow = ((ulong)random.Next() << 32) | (uint)random.Next();
var value = new GpuUInt128((UInt128)new GpuUInt128(valueHigh, valueLow) % (UInt128)modulus);
if (value.IsZero)
{
value = value.Add(1UL);
}
var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, (System.Numerics.BigInteger)(UInt128)modulus - 2, (System.Numerics.BigInteger)(UInt128)modulus);
value = value.ModInv(modulus);
((UInt128)value).Should().Be(expected);
}
}
[Fact]
public void ModInv_with_ulong_modulus_matches_BigInteger()
{
var random = new Random(8);
for (var i = 0; i < 20; i++)
{
ulong modulus = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
ulong raw = ((ulong)random.Next() << 32) | (uint)random.Next();
var value = new GpuUInt128((UInt128)(raw % modulus));
if (value.IsZero)
{
value = value.Add(1UL);
}
var expected = (UInt128)System.Numerics.BigInteger.ModPow((System.Numerics.BigInteger)(UInt128)value, (System.Numerics.BigInteger)modulus - 2, (System.Numerics.BigInteger)modulus);
value = value.ModInv(modulus);
((UInt128)value).Should().Be(expected);
}
}
private static UInt128 MulModReference(UInt128 a, UInt128 b, UInt128 modulus)
{
UInt128 result = 0;
UInt128 x = a;
UInt128 y = b;
while (y != 0)
{
if ((y & 1) != 0)
{
result += x;
if (result >= modulus)
{
result -= modulus;
}
}
x <<= 1;
if (x >= modulus)
{
x -= modulus;
}
y >>= 1;
}
return result;
}
}
@@ -0,0 +1,53 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Trait("Category", "Slow")]
public class LucasLehmerPrimeTesterTests
{
[Theory]
[InlineData(3UL, true)]
[InlineData(5UL, true)]
[InlineData(11UL, false)]
[Trait("Category", "Fast")]
public void IsMersennePrime_gpu_returns_expected_results(ulong p, bool expected)
{
new MersenneNumberLucasLehmerGpuTester().IsMersennePrime(p).Should().Be(expected);
}
[Fact]
[Trait("Category", "Fast")]
public void IsMersennePrime_gpu_large_exponent_without_cpu_fallback()
{
// Use a modest exponent that still exercises the NTT path while keeping
// runtime short enough for the unit test environment.
const ulong exponent = 128UL;
var tester = new MersenneNumberLucasLehmerGpuTester();
tester.WarmUpNttParameters(exponent);
tester.IsMersennePrime(exponent).Should().BeFalse();
}
[Theory]
[InlineData(132UL)]
[InlineData(1000UL)]
[Trait("Category", "Slow")]
public void WarmUpNttParameters_populates_cache_for_large_exponent(ulong exponent)
{
var tester = new MersenneNumberLucasLehmerGpuTester();
tester.WarmUpNttParameters(exponent);
tester.IsMersennePrime(exponent).Should().BeFalse();
}
[Theory]
[InlineData(3UL, true)]
[InlineData(5UL, true)]
[InlineData(11UL, false)]
[Trait("Category", "Fast")]
public void IsMersennePrime_cpu_matches_expected_results(ulong p, bool expected)
{
new MersenneNumberLucasLehmerCpuTester().IsPrime(p).Should().Be(expected);
}
}
@@ -0,0 +1,78 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
public class NttBackendConfigTests
{
[Fact]
[Trait("Category", "Fast")]
public void ForwardInverseGpu_reference_backend_matches_cpu_on_small_input()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(gpu);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(cpu);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
gpu.ToArray().Should().Equal(cpu.ToArray());
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void ForwardInverseGpu_staged_backend_matches_cpu_on_small_input()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(gpu);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(cpu);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
gpu.ToArray().Should().Equal(cpu.ToArray());
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
}
@@ -0,0 +1,361 @@
using System.Reflection;
using FluentAssertions;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu;
using System.Numerics;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
// GPU-related tests are organized per-backend so both reference and staged
// implementations are exercised under the fast test suite.
[Collection("GpuNtt")]
public class NttGpuMathTests
{
[Fact]
[Trait("Category", "Fast")]
public void ForwardGpu_reference_matches_cpu_forward()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
cpu[0] = new GpuUInt128(0UL, 1UL);
cpu[1] = new GpuUInt128(0UL, 2UL);
cpu[2] = new GpuUInt128(0UL, 3UL);
cpu[3] = new GpuUInt128(0UL, 4UL);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
cpu.CopyTo(gpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void ForwardGpu_staged_matches_cpu_forward()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
cpu[0] = new GpuUInt128(0UL, 1UL);
cpu[1] = new GpuUInt128(0UL, 2UL);
cpu[2] = new GpuUInt128(0UL, 3UL);
cpu[3] = new GpuUInt128(0UL, 4UL);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
cpu.CopyTo(gpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void InverseGpu_reference_matches_cpu_inverse()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> input = stackalloc GpuUInt128[4];
input[0] = new GpuUInt128(0UL, 5UL);
input[1] = new GpuUInt128(0UL, 7UL);
input[2] = new GpuUInt128(0UL, 9UL);
input[3] = new GpuUInt128(0UL, 11UL);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> transformed = stackalloc GpuUInt128[4];
input.CopyTo(transformed);
NttGpuMath.Forward(transformed, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
transformed.CopyTo(cpu);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
transformed.CopyTo(gpu);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < input.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void InverseGpu_staged_matches_cpu_inverse()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> input = stackalloc GpuUInt128[4];
input[0] = new GpuUInt128(0UL, 5UL);
input[1] = new GpuUInt128(0UL, 7UL);
input[2] = new GpuUInt128(0UL, 9UL);
input[3] = new GpuUInt128(0UL, 11UL);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> transformed = stackalloc GpuUInt128[4];
input.CopyTo(transformed);
NttGpuMath.Forward(transformed, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
transformed.CopyTo(cpu);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
transformed.CopyTo(gpu);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < input.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void Montgomery_twiddle_inverses_multiply_to_r2()
{
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
using var gpu = GpuContextPool.Rent();
var accelerator = gpu.Accelerator;
var method = typeof(NttGpuMath).GetMethod("GetSquareCache", BindingFlags.NonPublic | BindingFlags.Static);
method.Should().NotBeNull();
var cache = method!.Invoke(null, new object[] { accelerator, 8, modulus, primitiveRoot })!;
var cacheType = cache.GetType();
var twiddlesMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesMont")!.GetValue(cache)!;
var twiddlesInvMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesInvMont")!.GetValue(cache)!;
var r2 = (ulong)cacheType.GetProperty("MontR2Mod64")!.GetValue(cache)!;
var mod = (ulong)cacheType.GetProperty("ModulusLow")!.GetValue(cache)!;
int count = 7; // length 8 -> 7 twiddles
var forward = new GpuUInt128[count];
var inverse = new GpuUInt128[count];
twiddlesMont.CopyToCPU(forward);
twiddlesInvMont.CopyToCPU(inverse);
for (int i = 0; i < count; i++)
{
ulong prod = (ulong)((((BigInteger)forward[i].Low) * inverse[i].Low) % mod);
prod.Should().Be(r2);
}
}
[Fact]
[Trait("Category", "Fast")]
public void ConvolveGpu_reference_matches_cpu_convolve()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> leftCpu = stackalloc GpuUInt128[4];
Span<GpuUInt128> rightCpu = stackalloc GpuUInt128[4];
leftCpu[0] = new GpuUInt128(0UL, 1UL);
leftCpu[1] = new GpuUInt128(0UL, 2UL);
leftCpu[2] = new GpuUInt128(0UL, 3UL);
leftCpu[3] = new GpuUInt128(0UL, 4UL);
rightCpu[0] = new GpuUInt128(0UL, 5UL);
rightCpu[1] = new GpuUInt128(0UL, 6UL);
rightCpu[2] = new GpuUInt128(0UL, 7UL);
rightCpu[3] = new GpuUInt128(0UL, 8UL);
Span<GpuUInt128> leftGpu = stackalloc GpuUInt128[4];
Span<GpuUInt128> rightGpu = stackalloc GpuUInt128[4];
leftCpu.CopyTo(leftGpu);
rightCpu.CopyTo(rightGpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Convolve(leftCpu, rightCpu, modulus, primitiveRoot);
NttGpuMath.ConvolveGpu(leftGpu, rightGpu, modulus, primitiveRoot);
for (int i = 0; i < leftCpu.Length; i++)
{
leftGpu[i].Should().Be(leftCpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void ConvolveGpu_staged_matches_cpu_convolve()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> leftCpu = stackalloc GpuUInt128[4];
Span<GpuUInt128> rightCpu = stackalloc GpuUInt128[4];
leftCpu[0] = new GpuUInt128(0UL, 1UL);
leftCpu[1] = new GpuUInt128(0UL, 2UL);
leftCpu[2] = new GpuUInt128(0UL, 3UL);
leftCpu[3] = new GpuUInt128(0UL, 4UL);
rightCpu[0] = new GpuUInt128(0UL, 5UL);
rightCpu[1] = new GpuUInt128(0UL, 6UL);
rightCpu[2] = new GpuUInt128(0UL, 7UL);
rightCpu[3] = new GpuUInt128(0UL, 8UL);
Span<GpuUInt128> leftGpu = stackalloc GpuUInt128[4];
Span<GpuUInt128> rightGpu = stackalloc GpuUInt128[4];
leftCpu.CopyTo(leftGpu);
rightCpu.CopyTo(rightGpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Convolve(leftCpu, rightCpu, modulus, primitiveRoot);
NttGpuMath.ConvolveGpu(leftGpu, rightGpu, modulus, primitiveRoot);
for (int i = 0; i < leftCpu.Length; i++)
{
leftGpu[i].Should().Be(leftCpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void SquareGpu_reference_matches_cpu_square()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
cpu[0] = new GpuUInt128(0UL, 1UL);
cpu[1] = new GpuUInt128(0UL, 2UL);
cpu[2] = new GpuUInt128(0UL, 3UL);
cpu[3] = new GpuUInt128(0UL, 4UL);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
cpu.CopyTo(gpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Square(cpu, modulus, primitiveRoot);
NttGpuMath.SquareGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void SquareGpu_staged_matches_cpu_square()
{
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> cpu = stackalloc GpuUInt128[4];
cpu[0] = new GpuUInt128(0UL, 1UL);
cpu[1] = new GpuUInt128(0UL, 2UL);
cpu[2] = new GpuUInt128(0UL, 3UL);
cpu[3] = new GpuUInt128(0UL, 4UL);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[4];
cpu.CopyTo(gpu);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
NttGpuMath.Square(cpu, modulus, primitiveRoot);
NttGpuMath.SquareGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
[Fact]
[Trait("Category", "Fast")]
public void BitReverseGpu_matches_cpu()
{
GpuUInt128[] cpu = new GpuUInt128[8];
for (ulong i = 0; i < (ulong)cpu.Length; i++)
{
cpu[i] = new GpuUInt128(0UL, i);
}
GpuUInt128[] gpu = (GpuUInt128[])cpu.Clone();
NttGpuMath.BitReverse(cpu);
NttGpuMath.BitReverseGpu(gpu);
gpu.Should().Equal(cpu);
}
[Fact]
[Trait("Category", "Fast")]
public void DisposeAll_clears_ntt_caches()
{
// Ensure the cache starts empty.
NttGpuMath.ClearCaches();
Span<GpuUInt128> values = stackalloc GpuUInt128[2];
values[0] = new GpuUInt128(0UL, 1UL);
values[1] = new GpuUInt128(0UL, 2UL);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
using (var gpu = GpuContextPool.Rent())
{
var buffer = gpu.Accelerator.Allocate1D<GpuUInt128>(values.Length);
buffer.View.CopyFromCPU(ref values[0], values.Length);
NttGpuMath.SquareDevice(gpu.Accelerator, buffer.View, modulus, primitiveRoot);
buffer.Dispose();
}
// Verify that the square cache now holds entries for the accelerator.
var field = typeof(NttGpuMath).GetField("SquareCache", BindingFlags.NonPublic | BindingFlags.Static)!;
var cacheObj = field.GetValue(null)!;
int count = (int)cacheObj.GetType().GetProperty("Count")!.GetValue(cacheObj)!;
count.Should().BeGreaterThan(0);
GpuContextPool.DisposeAll();
count = (int)cacheObj.GetType().GetProperty("Count")!.GetValue(cacheObj)!;
count.Should().Be(0);
}
}
@@ -0,0 +1,87 @@
using FluentAssertions;
using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
[Trait("Category", "Fast")]
public class NttMontgomeryTests
{
[Fact]
[Trait("Category", "Fast")]
public void SquareDevice_montgomery64_matches_cpu_square_on_small_input()
{
// Small 64-bit modulus and tiny input to keep runtime trivial.
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)cpu.Length; i++)
{
cpu[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
Span<GpuUInt128> gpu = stackalloc GpuUInt128[16];
cpu.CopyTo(gpu);
for (int i = cpu.Length; i < gpu.Length; i++)
{
gpu[i] = new GpuUInt128(0UL, 0UL);
}
// CPU reference
NttGpuMath.Square(cpu, modulus, primitiveRoot);
// Device execution
using var context = GpuContextPool.Rent();
var accelerator = context.Accelerator;
using var buffer = accelerator.Allocate1D<GpuUInt128>(gpu.Length);
buffer.View.CopyFromCPU(ref gpu[0], gpu.Length);
NttGpuMath.SquareDevice(accelerator, buffer.View, modulus, primitiveRoot);
buffer.View.CopyToCPU(ref gpu[0], gpu.Length);
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
[Fact]
[Trait("Category", "Fast")]
public void InverseDevice_montgomery64_matches_cpu_inverse_on_small_input()
{
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
Span<GpuUInt128> transformed = stackalloc GpuUInt128[input.Length];
input.CopyTo(transformed);
NttGpuMath.Forward(transformed, modulus, primitiveRoot);
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
transformed.CopyTo(cpu);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
var original = NttGpuMath.GpuTransformBackend;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
transformed.CopyTo(gpu);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
for (int i = 0; i < input.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
NttGpuMath.GpuTransformBackend = original;
}
}
}
@@ -0,0 +1,90 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
public class NttReductionModeTests
{
[Theory]
[Trait("Category", "Fast")]
[InlineData(ModReductionMode.Auto)]
[InlineData(ModReductionMode.GpuUInt128)]
public void ForwardInverse_small64bit_modulus_reference_backend_works_with_mode(ModReductionMode mode)
{
var originalBackend = NttGpuMath.GpuTransformBackend;
var originalMode = NttGpuMath.ReductionMode;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
NttGpuMath.ReductionMode = mode;
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(cpu);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(gpu);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
gpu.ToArray().Should().Equal(cpu.ToArray());
}
finally
{
NttGpuMath.GpuTransformBackend = originalBackend;
NttGpuMath.ReductionMode = originalMode;
}
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(ModReductionMode.Auto)]
[InlineData(ModReductionMode.GpuUInt128)]
public void ForwardInverse_small64bit_modulus_staged_backend_works_with_mode(ModReductionMode mode)
{
var originalBackend = NttGpuMath.GpuTransformBackend;
var originalMode = NttGpuMath.ReductionMode;
try
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
NttGpuMath.ReductionMode = mode;
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
Span<GpuUInt128> input = stackalloc GpuUInt128[8];
for (ulong i = 0; i < (ulong)input.Length; i++)
{
input[(int)i] = new GpuUInt128(0UL, i + 1UL);
}
Span<GpuUInt128> cpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(cpu);
NttGpuMath.Forward(cpu, modulus, primitiveRoot);
NttGpuMath.Inverse(cpu, modulus, primitiveRoot);
Span<GpuUInt128> gpu = stackalloc GpuUInt128[input.Length];
input.CopyTo(gpu);
NttGpuMath.ForwardGpu(gpu, modulus, primitiveRoot);
NttGpuMath.InverseGpu(gpu, modulus, primitiveRoot);
gpu.ToArray().Should().Equal(cpu.ToArray());
}
finally
{
NttGpuMath.GpuTransformBackend = originalBackend;
NttGpuMath.ReductionMode = originalMode;
}
}
}
@@ -0,0 +1,39 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Collection("GpuNtt")]
public class GpuKernelPoolTests
{
[Fact]
[Trait("Category", "Fast")]
public void EnsureSmallCyclesOnDevice_returns_same_view_on_subsequent_calls()
{
using var lease = GpuContextPool.RentPreferred(preferCpu: true);
var accelerator = lease.Accelerator;
var first = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
var second = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
second.Equals(first).Should().BeTrue();
second.Length.Should().Be(MersenneDivisorCycles.SmallDivisorsMax + 1);
}
[Fact]
[Trait("Category", "Fast")]
public void Run_provides_accelerator_and_stream()
{
bool executed = false;
GpuKernelPool.Run((acc, stream) =>
{
acc.Should().NotBeNull();
stream.Should().NotBeNull();
executed = true;
});
executed.Should().BeTrue();
}
}
@@ -0,0 +1,39 @@
using System.Diagnostics;
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Collection("GpuNtt")]
public class GpuWorkLimiterTests
{
[Fact]
[Trait("Category", "Fast")]
public async Task Acquire_blocks_when_limit_reached()
{
GpuWorkLimiter.SetLimit(1);
var startedSecond = new TaskCompletionSource<bool>();
var sw = new Stopwatch();
using var first = GpuWorkLimiter.Acquire();
sw.Start();
var secondTask = Task.Run(() =>
{
using var second = GpuWorkLimiter.Acquire();
startedSecond.TrySetResult(true);
});
await Task.Delay(50);
startedSecond.Task.IsCompleted.Should().BeFalse();
first.Dispose();
await secondTask.WaitAsync(TimeSpan.FromSeconds(1));
sw.Stop();
sw.ElapsedMilliseconds.Should().BeGreaterThanOrEqualTo(50);
}
}
@@ -0,0 +1,12 @@
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class InputParserTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
}
@@ -0,0 +1,12 @@
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class KRangeFinderTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
}
@@ -0,0 +1,25 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class MersenneDivisorCyclesTests
{
public static IEnumerable<object[]> Divisors() =>
new[]
{
new object[] {3UL, 2UL},
new object[] {7UL, 3UL},
new object[] {11UL, 10UL},
};
[Theory]
[Trait("Category", "Fast")]
[MemberData(nameof(Divisors))]
public void CalculateCycleLength_returns_expected_value(ulong divisor, ulong expected)
{
MersenneDivisorCycles.CalculateCycleLength(divisor).Should().Be(expected);
MersenneDivisorCycles.CalculateCycleLengthGpu(divisor).Should().Be(expected);
}
}
@@ -0,0 +1,36 @@
using FluentAssertions;
using PerfectNumbers.Core.Cpu;
using Xunit;
using UInt128 = System.UInt128;
namespace PerfectNumbers.Core.Tests;
public class MersenneNumberIncrementalCpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
[Theory]
[InlineData(GpuKernelType.Incremental)]
[InlineData(GpuKernelType.Pow2Mod)]
[Trait("Category", "Fast")]
public void Scan_handles_various_prime_exponents(GpuKernelType type)
{
var tester = new MersenneNumberIncrementalCpuTester(type);
if (type == GpuKernelType.Pow2Mod)
{
RunCase(tester, 11UL, 1UL, expectedPrime: false);
RunCase(tester, 71UL, 341_860UL, expectedPrime: false);
}
RunCase(tester, 89UL, 1_000UL, expectedPrime: true);
RunCase(tester, 127UL, 1_000UL, expectedPrime: true);
}
private static void RunCase(MersenneNumberIncrementalCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -0,0 +1,37 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
using UInt128 = System.UInt128;
namespace PerfectNumbers.Core.Tests;
public class MersenneNumberIncrementalGpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
[Theory]
[InlineData(GpuKernelType.Pow2Mod, false)]
[InlineData(GpuKernelType.Incremental, false)]
[InlineData(GpuKernelType.Pow2Mod, true)]
[Trait("Category", "Fast")]
public void Scan_handles_various_prime_exponents(GpuKernelType type, bool useGpuOrder)
{
var tester = new MersenneNumberIncrementalGpuTester(type, useGpuOrder);
if (type == GpuKernelType.Pow2Mod)
{
RunCase(tester, 11UL, 1UL, expectedPrime: false);
RunCase(tester, 71UL, 341_860UL, expectedPrime: false);
}
RunCase(tester, 89UL, 1_000UL, expectedPrime: true);
RunCase(tester, 127UL, 1_000UL, expectedPrime: true);
}
private static void RunCase(MersenneNumberIncrementalGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -0,0 +1,37 @@
using FluentAssertions;
using PerfectNumbers.Core.Cpu;
using Xunit;
using UInt128 = System.UInt128;
namespace PerfectNumbers.Core.Tests;
public class MersenneNumberOrderCpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
[Theory]
[InlineData(GpuKernelType.Incremental)]
[InlineData(GpuKernelType.Pow2Mod)]
[Trait("Category", "Fast")]
public void Scan_handles_various_prime_exponents(GpuKernelType type)
{
var tester = new MersenneNumberOrderCpuTester(type);
if (type == GpuKernelType.Pow2Mod)
{
RunCase(tester, 23UL, 1UL, expectedPrime: false);
RunCase(tester, 29UL, 36UL, expectedPrime: false);
}
RunCase(tester, 89UL, 1_000UL, expectedPrime: true);
RunCase(tester, 127UL, 1_000UL, expectedPrime: true);
}
private static void RunCase(MersenneNumberOrderCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -0,0 +1,38 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
using UInt128 = System.UInt128;
namespace PerfectNumbers.Core.Tests;
public class MersenneNumberOrderGpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
[Theory]
[InlineData(GpuKernelType.Pow2Mod, false)]
[InlineData(GpuKernelType.Incremental, false)]
[InlineData(GpuKernelType.Pow2Mod, true)]
[Trait("Category", "Fast")]
public void Scan_handles_various_prime_exponents(GpuKernelType type, bool useGpuOrder)
{
var tester = new MersenneNumberOrderGpuTester(type, useGpuOrder);
if (type == GpuKernelType.Pow2Mod)
{
RunCase(tester, 23UL, 1UL, expectedPrime: false);
RunCase(tester, 29UL, 36UL, expectedPrime: false);
}
RunCase(tester, 89UL, 1_000UL, expectedPrime: true);
RunCase(tester, 127UL, 1_000UL, expectedPrime: true);
}
private static void RunCase(MersenneNumberOrderGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -0,0 +1,31 @@
using FluentAssertions;
using PerfectNumbers.Core.Cpu;
using Xunit;
using UInt128 = System.UInt128;
namespace PerfectNumbers.Core.Tests;
public class MersenneNumberResidueCpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
[Fact]
[Trait("Category", "Fast")]
public void Scan_handles_various_prime_exponents()
{
var tester = new MersenneNumberResidueCpuTester();
RunCase(tester, 23UL, 1UL, expectedPrime: false);
RunCase(tester, 29UL, 36UL, expectedPrime: false);
RunCase(tester, 89UL, 1_000UL, expectedPrime: true);
RunCase(tester, 127UL, 1_000UL, expectedPrime: true);
}
private static void RunCase(MersenneNumberResidueCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -0,0 +1,33 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
using UInt128 = System.UInt128;
namespace PerfectNumbers.Core.Tests;
public class MersenneNumberResidueGpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
[Theory]
[InlineData(false)]
[InlineData(true)]
[Trait("Category", "Fast")]
public void Scan_handles_various_prime_exponents(bool useGpuOrder)
{
var tester = new MersenneNumberResidueGpuTester(useGpuOrder);
RunCase(tester, 23UL, 2UL, expectedPrime: false);
RunCase(tester, 29UL, 37UL, expectedPrime: false);
RunCase(tester, 89UL, 1_001UL, expectedPrime: true);
RunCase(tester, 127UL, 1_001UL, expectedPrime: true);
}
private static void RunCase(MersenneNumberResidueGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -0,0 +1,36 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Collection("GpuNtt")]
[Trait("Category", "Fast")]
public class MersenneNumberTesterTests
{
[Theory]
[InlineData(136_000_002UL)]
[InlineData(136_000_005UL)]
public void IsMersennePrime_detects_small_prime_divisors_for_large_exponents(ulong p)
{
var tester = new MersenneNumberTester();
tester.IsMersennePrime(p).Should().BeFalse();
}
[Theory]
[InlineData(125UL)]
public void WarmUpOrders_populates_cache_without_affecting_results(ulong p)
{
var tester = new MersenneNumberTester(useOrderCache: true, useGpuOrder: true);
tester.WarmUpOrders(p, 1_000UL);
tester.IsMersennePrime(p).Should().BeFalse();
}
[Theory]
[InlineData(125UL, true)]
[InlineData(127UL, false)]
public void SharesFactorWithExponentMinusOne_detects_common_factors(ulong p, bool expected)
{
p.SharesFactorWithExponentMinusOne().Should().Be(expected);
}
}
@@ -0,0 +1,16 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class MersenneResidueModeTests
{
[Fact]
public void IsMersennePrime_residue_mode_rejects_composite_exponent()
{
var tester = new MersenneNumberTester(useResidue: true, useIncremental: false);
tester.IsMersennePrime(136_000_002UL).Should().BeFalse();
}
}
@@ -0,0 +1,29 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class MersennePrimeFactorTesterTests
{
[Theory]
[InlineData(5UL, 31UL, true)]
[InlineData(11UL, 23UL, true)]
[InlineData(11UL, 89UL, true)]
[InlineData(5UL, 7UL, false)]
[InlineData(7UL, 17UL, false)]
[InlineData(11UL, 331UL, false)]
[Trait("Category", "Fast")]
public void IsPrimeFactor_identifies_divisibility(ulong p, ulong q, bool expected)
{
MersennePrimeFactorTester.IsPrimeFactor(p, q, CancellationToken.None).Should().Be(expected);
}
[Fact]
[Trait("Category", "Slow")]
public void GetOrderOf2ModPrime_handles_large_divisors()
{
UInt128 q = UInt128.Parse("618970019642690137449562111");
MersennePrimeFactorTester.GetOrderOf2ModPrime(q, CancellationToken.None)
.Should().Be((UInt128)89);
}
}
@@ -0,0 +1,37 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class MersenneResidueStepperTests
{
[Fact]
[Trait("Category", "Fast")]
public void Step_advances_residue()
{
var stepper = new MersenneResidueStepper(13, 4, 2);
stepper.State().Should().Be((4UL, 2UL));
stepper.Step(3).Should().Be((7UL, 10UL));
}
[Fact]
[Trait("Category", "Fast")]
public void To_moves_directly_to_target_exponent()
{
var stepper = new MersenneResidueStepper(13, 4, 2);
stepper.To(10).Should().Be((10UL, 9UL));
}
[Fact]
[Trait("Category", "Fast")]
public void Constructor_rejects_zero_modulus()
{
var act = () => new MersenneResidueStepper(0, 0, 0);
act.Should().Throw<ArgumentException>();
}
}
@@ -0,0 +1,66 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class ModResidueTrackerTests
{
[Fact]
public void IsDivisible_advances_residue_for_identity()
{
var tracker = new ModResidueTracker(ResidueModel.Identity);
tracker.IsDivisible(5, 2).Should().BeFalse();
tracker.IsDivisible(6, 2).Should().BeTrue();
tracker.IsDivisible(7, 2).Should().BeFalse();
}
[Theory]
[InlineData(3UL, 7UL, true)]
[InlineData(5UL, 31UL, true)]
[InlineData(5UL, 7UL, false)]
[InlineData(11UL, 23UL, true)]
[InlineData(11UL, 31UL, false)]
public void IsDivisible_handles_mersenne_model(ulong p, ulong divisor, bool expected)
{
var tracker = new ModResidueTracker(ResidueModel.Mersenne);
tracker.IsDivisible(p, divisor).Should().Be(expected);
}
[Fact]
public void MergeOrAppend_tracks_existing_and_new_divisors()
{
var tracker = new ModResidueTracker(ResidueModel.Identity);
tracker.BeginMerge(10);
tracker.MergeOrAppend(10, 3, out bool d3).Should().BeTrue();
d3.Should().BeFalse();
tracker.MergeOrAppend(10, 5, out bool d5).Should().BeTrue();
d5.Should().BeTrue();
tracker.BeginMerge(10);
tracker.MergeOrAppend(10, 4, out bool d4).Should().BeTrue();
d4.Should().BeFalse();
tracker.BeginMerge(11);
tracker.MergeOrAppend(11, 3, out d3).Should().BeTrue();
d3.Should().BeFalse();
tracker.MergeOrAppend(11, 4, out d4).Should().BeTrue();
d4.Should().BeFalse();
tracker.MergeOrAppend(11, 5, out d5).Should().BeTrue();
d5.Should().BeFalse();
}
[Fact]
public void IsDivisible_requires_non_decreasing_numbers()
{
var tracker = new ModResidueTracker(ResidueModel.Identity);
tracker.IsDivisible(5, 2);
Action act = () => tracker.IsDivisible(4, 2);
act.Should().Throw<InvalidOperationException>();
}
}
@@ -0,0 +1,91 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class ModuloAutomatonTests
{
[Fact]
public void Ending7Automaton_mod9_hits_every_9th()
{
// Start below 7; sequence: 7, 17, 27, 37, ...
var auto = new Ending7Automaton(1, 9);
int hits = 0;
ulong firstHit = 0;
for (int i = 0; i < 40; i++)
{
ulong n = auto.Current();
bool div9 = auto.DivisibleBy(9);
if (div9)
{
hits++;
if (firstHit == 0)
{
firstHit = n;
}
}
auto.Next();
}
// Known CRT class: n ≡ 27 (mod 90) when n ends with 7 and n ≡ 0 (mod 9)
firstHit.Should().Be(27);
hits.Should().BeGreaterThan(1);
}
[Theory]
[InlineData(13UL, 2000)]
[InlineData(17UL, 2000)]
public void MersenneResidueAutomaton_matches_direct_mods(ulong p, int count)
{
// Compare residue progression against direct modular computations
var auto = new MersenneResidueAutomaton(p);
var twoP = (System.UInt128)p << 1;
var q = twoP + 1UL;
for (int i = 0; i < count; i++)
{
ulong r10 = Mod10(q);
ulong r8 = (ulong)q & 7UL;
ulong r3 = Mod3(q);
ulong r5 = Mod5(q);
auto.Mod10R.Should().Be(r10);
auto.Mod8R.Should().Be(r8);
auto.Mod3R.Should().Be(r3);
auto.Mod5R.Should().Be(r5);
auto.Next();
q += twoP;
}
}
private static ulong Mod10(System.UInt128 value)
{
ulong high = (ulong)(value >> 64);
ulong low = (ulong)value;
ulong highRem = Mod10(high);
ulong lowRem = Mod10(low);
ulong combined = lowRem + highRem * 6UL;
return Mod10(combined);
}
private static ulong Mod10(ulong value) => value % 10UL;
private static ulong Mod3(System.UInt128 value)
{
ulong high = (ulong)(value >> 64);
ulong low = (ulong)value;
ulong rem = (low % 3UL) + (high % 3UL);
return rem >= 3UL ? rem - 3UL : rem;
}
private static ulong Mod5(System.UInt128 value)
{
ulong high = (ulong)(value >> 64);
ulong low = (ulong)value;
ulong rem = (low % 5UL) + (high % 5UL);
return rem >= 5UL ? rem - 5UL : rem;
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../PerfectNumbers.Core/PerfectNumbers.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,27 @@
using System.Numerics;
using FluentAssertions;
using PeterO.Numbers;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class PowerCacheTests
{
[Fact]
[Trait("Category", "Fast")]
public void Get_returns_big_integer_power()
{
PowerCache.Get(2UL, 5).Should().Be(new BigInteger(32));
PowerCache.Get(2UL, 10).Should().Be(new BigInteger(1024));
}
[Fact]
[Trait("Category", "Fast")]
public void Get_returns_einteger_power()
{
EInteger baseVal = EInteger.FromInt32(3);
PowerCache.Get(baseVal, 4).Should().Be(EInteger.FromInt32(81));
PowerCache.Get(baseVal, 2).Should().Be(EInteger.FromInt32(9));
}
}
@@ -0,0 +1,39 @@
using FluentAssertions;
using PeterO.Numbers;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class PrimeCacheTests
{
[Fact]
[Trait("Category", "Fast")]
public void GetEulerPrimes_returns_primes_congruent_to_one_mod_four()
{
var cache = new PrimeCache();
EInteger[] result = cache.GetEulerPrimes(EInteger.FromInt32(2), EInteger.FromInt32(20));
result.Should().Equal(
EInteger.FromInt32(5),
EInteger.FromInt32(13),
EInteger.FromInt32(17));
}
[Fact]
[Trait("Category", "Fast")]
public void EnumeratePrimes_returns_sequence_starting_from_value()
{
var cache = new PrimeCache();
EInteger[] result = cache.EnumeratePrimes(EInteger.FromInt32(10))
.Take(5)
.ToArray();
result.Should().Equal(
EInteger.FromInt32(11),
EInteger.FromInt32(13),
EInteger.FromInt32(17),
EInteger.FromInt32(19),
EInteger.FromInt32(23));
}
}
@@ -0,0 +1,25 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class PrimeTesterGcdTests
{
[Theory]
[InlineData(81UL, true)]
[InlineData(101UL, false)]
public void SharesFactorWithMaxExponent_detects_gcd(ulong n, bool expected)
{
PrimeTester.SharesFactorWithMaxExponent(n).Should().Be(expected);
}
[Fact]
public void SharesFactorWithMaxExponentBatch_filters_values()
{
ulong[] values = [81UL, 101UL];
Span<byte> results = stackalloc byte[values.Length];
PrimeTester.SharesFactorWithMaxExponentBatch(values, results);
results.ToArray().Should().Equal(new byte[] { 1, 0 });
}
}
@@ -0,0 +1,39 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class PrimeTesterIsPrimeTests
{
[Theory]
[Trait("Category", "Fast")]
[InlineData(1UL, false)]
[InlineData(2UL, true)]
[InlineData(3UL, true)]
[InlineData(4UL, false)]
[InlineData(7UL, true)]
[InlineData(97UL, true)]
[InlineData(121UL, false)]
public void IsPrime_returns_expected_results(ulong n, bool expected)
{
var tester = new PrimeTester();
tester.IsPrime(n, CancellationToken.None).Should().Be(expected);
}
[Fact]
[Trait("Category", "Fast")]
public void IsPrimeGpu_falls_back_to_cpu_when_forced()
{
var tester = new PrimeTester();
GpuContextPool.ForceCpu = true;
tester.IsPrimeGpu(11UL, CancellationToken.None).Should().BeTrue();
tester.IsPrimeGpu(12UL, CancellationToken.None).Should().BeFalse();
GpuContextPool.ForceCpu = false;
}
}
@@ -0,0 +1,25 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class PrimesGeneratorTests
{
[Fact]
[Trait("Category", "Fast")]
public void Small_primes_and_squares_match()
{
var primes = PrimesGenerator.SmallPrimes;
var squares = PrimesGenerator.SmallPrimesPow2;
primes[..5].Should().Equal(new ulong[] { 2UL, 3UL, 5UL, 7UL, 11UL });
squares[..5].Should().Equal(new ulong[] { 4UL, 9UL, 25UL, 49UL, 121UL });
for (int i = 0; i < 5; i++)
{
squares[i].Should().Be(primes[i] * primes[i]);
}
}
}
@@ -0,0 +1,53 @@
using System.Numerics;
using FluentAssertions;
using PeterO.Numbers;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class RationalHelperTests
{
[Fact]
[Trait("Category", "Fast")]
public void FromDoubleRounded_ReducesFraction()
{
ERational result = RationalHelper.FromDoubleRounded(0.75);
result.Numerator.ToInt32Checked().Should().Be(3);
result.Denominator.ToInt32Checked().Should().Be(4);
}
[Fact]
[Trait("Category", "Fast")]
public void ToEInteger_ReturnsMatchingValue()
{
BigInteger value = BigInteger.Parse("12345678901234567890");
EInteger einteger = RationalHelper.ToEInteger(value);
einteger.ToString().Should().Be(value.ToString());
}
[Fact]
[Trait("Category", "Fast")]
public void FloorToUInt64_ReturnsFloor()
{
ERational value = ERational.Create(5, 2);
ulong result = RationalHelper.FloorToUInt64(value);
result.Should().Be(2UL);
}
[Fact]
[Trait("Category", "Fast")]
public void CeilingToUInt64_RoundsUpWhenFractional()
{
ERational fractional = ERational.Create(5, 2);
ERational integral = ERational.Create(6, 2);
RationalHelper.CeilingToUInt64(fractional).Should().Be(3UL);
RationalHelper.CeilingToUInt64(integral).Should().Be(3UL);
}
}
@@ -0,0 +1,12 @@
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class RleBlacklistTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
}
@@ -0,0 +1,36 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class StringBuilderPoolTests
{
[Fact]
[Trait("Category", "Fast")]
public void Rent_ReturnsClearedBuilder()
{
var sb = StringBuilderPool.Rent();
sb.Length.Should().Be(0);
StringBuilderPool.Return(sb);
}
[Fact]
[Trait("Category", "Fast")]
public void Return_AllowsReuseOfBuilder()
{
var sb = StringBuilderPool.Rent();
sb.Append("data");
StringBuilderPool.Return(sb);
var reused = StringBuilderPool.Rent();
reused.Should().BeSameAs(sb);
reused.Length.Should().Be(0);
StringBuilderPool.Return(reused);
}
}
@@ -0,0 +1,12 @@
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class TextFileWriterTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
}
@@ -0,0 +1,218 @@
using System.Numerics;
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class UInt128ExtensionsTests
{
public static IEnumerable<object[]> BinaryGcdCases()
{
yield return new object[] { UInt128.Zero, (UInt128)5, (UInt128)5 };
yield return new object[] { (UInt128)18, (UInt128)24, (UInt128)6 };
UInt128 big1 = (UInt128.One << 96) + 12345;
UInt128 big2 = (UInt128.One << 80) + 67890;
yield return new object[]
{
big1,
big2,
(UInt128)BigInteger.GreatestCommonDivisor((BigInteger)big1, (BigInteger)big2)
};
}
[Theory]
[Trait("Category", "Fast")]
[MemberData(nameof(BinaryGcdCases))]
public void BinaryGcd_matches_BigInteger(UInt128 a, UInt128 b, UInt128 expected)
{
a.BinaryGcd(b).Should().Be(expected);
}
public static IEnumerable<object[]> TrailingZeroCases()
{
yield return new object[] { UInt128.Zero, 128 };
yield return new object[] { (UInt128)1, 0 };
yield return new object[] { (UInt128)8, 3 };
yield return new object[] { UInt128.One << 80, 80 };
}
[Theory]
[Trait("Category", "Fast")]
[MemberData(nameof(TrailingZeroCases))]
public void CountTrailingZeros_returns_expected(UInt128 value, int expected)
{
value.CountTrailingZeros().Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(3UL)]
[InlineData(5UL)]
[InlineData(7UL)]
public void CalculateOrder_matches_naive(ulong q)
{
UInt128 q128 = q;
ulong expected = NaiveOrder(q);
q128.CalculateOrder().Should().Be(expected);
}
private static ulong NaiveOrder(ulong q)
{
if (q <= 2UL)
{
return 0UL;
}
ulong order = 1UL;
ulong value = 2UL % q;
while (value != 1UL)
{
value = (value << 1) % q;
order++;
}
return order;
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(2UL, true)]
[InlineData(9UL, false)]
[InlineData(97UL, true)]
public void IsPrimeCandidate_checks_small(ulong n, bool expected)
{
((UInt128)n).IsPrimeCandidate().Should().Be(expected);
}
[Fact]
[Trait("Category", "Fast")]
public void IsPrimeCandidate_detects_large_composite()
{
UInt128 n = (UInt128.One << 64) + 1;
n.IsPrimeCandidate().Should().BeFalse();
}
[Fact]
[Trait("Category", "Fast")]
public void Mod10_8_5_3_matches_operator()
{
UInt128 value = (UInt128.One << 100) + 12345678901234567890UL;
value.Mod10_8_5_3(out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3);
ulong expected10 = (ulong)(value % 10);
ulong expected8 = (ulong)(value % 8);
ulong expected5 = (ulong)(value % 5);
ulong expected3 = (ulong)(value % 3);
mod10.Should().Be(expected10);
mod8.Should().Be(expected8);
mod5.Should().Be(expected5);
mod3.Should().Be(expected3);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(2UL, 10UL, 17UL)]
[InlineData(123456789UL, 3UL, 97UL)]
public void ModPow_matches_BigInteger(ulong value, ulong exponent, ulong modulus)
{
UInt128 v = value;
UInt128 e = exponent;
UInt128 m = modulus;
UInt128 expected = (UInt128)BigInteger.ModPow(value, exponent, modulus);
v.ModPow(e, m).Should().Be(expected);
}
[Fact]
[Trait("Category", "Fast")]
public void ModPow_handles_large_values()
{
UInt128 v = (UInt128.One << 96) + 12345;
UInt128 e = 17;
UInt128 m = 97;
UInt128 expected = (UInt128)BigInteger.ModPow((BigInteger)v, (BigInteger)e, (BigInteger)m);
v.ModPow(e, m).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(1UL)]
[InlineData(123456789UL)]
public void Mod10_matches_operator(ulong low)
{
UInt128 value = (UInt128.One << 100) + low;
value.Mod10().Should().Be((ulong)(value % 10));
}
[Fact]
[Trait("Category", "Fast")]
public void Mod8_matches_operator()
{
UInt128 value = (UInt128.One << 100) + 12345;
value.Mod8().Should().Be((ulong)(value % 8));
}
[Fact]
[Trait("Category", "Fast")]
public void Mod3_matches_operator()
{
UInt128 value = (UInt128.One << 100) + 12345;
value.Mod3().Should().Be((ulong)(value % 3));
}
[Fact]
[Trait("Category", "Fast")]
public void Mod5_matches_operator()
{
UInt128 value = (UInt128.One << 100) + 12345;
value.Mod5().Should().Be((ulong)(value % 5));
}
[Fact]
[Trait("Category", "Fast")]
public void Mul64_matches_expected_product()
{
UInt128 a = (UInt128.One << 80) + 123456789;
UInt128 b = (UInt128.One << 64) + 987654321;
UInt128 expected = (UInt128)((UInt128)(ulong)a * b);
a.Mul64(b).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(123UL, 456UL, 789UL)]
[InlineData(123456789UL, 987654321UL, 1009UL)]
public void MulMod_matches_BigInteger(ulong a, ulong b, ulong modulus)
{
UInt128 ua = a % modulus;
UInt128 ub = b % modulus;
UInt128 mod = modulus;
UInt128 expected = (UInt128)(((BigInteger)ua * (BigInteger)ub) % modulus);
ua.MulMod(ub, mod).Should().Be(expected);
}
[Fact]
[Trait("Category", "Fast")]
public void MulMod_handles_large_inputs()
{
UInt128 a = (UInt128.One << 80) + 12345;
UInt128 b = (UInt128.One << 96) + 67890;
UInt128 mod = (UInt128.One << 100) - 93;
UInt128 expected = (UInt128)(((BigInteger)a * (BigInteger)b) % (BigInteger)mod);
a.MulMod(b, mod).Should().Be(expected);
}
}
@@ -0,0 +1,185 @@
using System.Numerics;
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class ULongExtensionsTests
{
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(1UL)]
[InlineData(9UL)]
[InlineData(10UL)]
[InlineData(1234567890UL)]
[InlineData(ulong.MaxValue)]
public void Mod10_returns_same_as_operator(ulong value)
{
ulong expected = value % 10UL;
ULongExtensions.Mod10(value).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(2UL, true)]
[InlineData(9UL, false)]
[InlineData(97UL, true)]
public void IsPrimeCandidate_filters_composites(ulong candidate, bool expected)
{
candidate.IsPrimeCandidate().Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(3UL)]
[InlineData(5UL)]
[InlineData(7UL)]
[InlineData(11UL)]
public void CalculateOrder_matches_naive_algorithm(ulong q)
{
ulong expected = NaiveOrder(q);
q.CalculateOrder().Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(2UL, 10UL, 17UL)]
[InlineData(123456789UL, 3UL, 97UL)]
public void ModPow64_matches_BigInteger(ulong value, ulong exponent, ulong modulus)
{
ulong expected = (ulong)BigInteger.ModPow(value, exponent, modulus);
value.ModPow64(exponent, modulus).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(10UL, 17UL)]
[InlineData(123UL, 97UL)]
public void PowMod_returns_same_as_BigInteger(ulong exponent, ulong modulus)
{
UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus);
exponent.PowMod((UInt128)modulus).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(5UL, 7UL, 3UL, 4UL)]
[InlineData(10UL, 7UL, 3UL, 2UL)]
public void PowModWithCycle_uses_cycle_length(ulong exponent, ulong modulus, ulong cycle, ulong expected)
{
((UInt128)exponent).PowModWithCycle((UInt128)modulus, cycle).Should().Be((UInt128)expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(15UL, true)]
[InlineData(7UL, false)]
[InlineData(9UL, false)]
public void SharesFactorWithExponentMinusOne_detects_relationship(ulong exponent, bool expected)
{
exponent.SharesFactorWithExponentMinusOne().Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(1234567890123456789UL, 3UL)]
[InlineData(ulong.MaxValue, 7UL)]
public void FastDiv64_matches_division(ulong value, ulong divisor)
{
ulong mul = (ulong)(((UInt128)1 << 64) / divisor);
value.FastDiv64(divisor, mul).Should().Be(value / divisor);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL, 0UL)]
[InlineData(ulong.MaxValue, 1UL)]
[InlineData(123456789UL, 987654321UL)]
public void MulHigh_matches_high_bits(ulong x, ulong y)
{
ulong expected = (ulong)(((UInt128)x * y) >> 64);
x.MulHigh(y).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL, 0UL)]
[InlineData(ulong.MaxValue, 2UL)]
[InlineData(123456789UL, 987654321UL)]
public void Mul64_matches_UInt128_product(ulong a, ulong b)
{
UInt128 expected = (UInt128)a * b;
a.Mul64(b).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(123456789UL, 987654321UL, 97UL)]
[InlineData(ulong.MaxValue, ulong.MaxValue, 1009UL)]
public void MulMod64_matches_BigInteger(ulong a, ulong b, ulong modulus)
{
ulong expected = (ulong)BigInteger.Remainder((BigInteger)a * b, modulus);
a.MulMod64(b, modulus).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(100UL, 7UL, 3UL)]
[InlineData(123UL, 9UL, 6UL)]
public void PowModWithCycle_ulong_overload_uses_cycle(ulong exponent, ulong modulus, ulong cycle)
{
UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus);
exponent.PowModWithCycle((UInt128)modulus, cycle).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(100UL, 7UL, 3UL)]
[InlineData(123UL, 9UL, 6UL)]
public void PowModWithCycle_ulong_uint128_cycle_uses_cycle(ulong exponent, ulong modulus, ulong cycle)
{
UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus);
exponent.PowModWithCycle((UInt128)modulus, (UInt128)cycle).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(11UL, 13UL)]
public void PowModCrt_matches_BigInteger(ulong exponent, ulong modulus)
{
MersenneDivisorCycles cycles = MersenneDivisorCycles.Shared;
UInt128 expected = (UInt128)BigInteger.ModPow(2, exponent, modulus);
exponent.PowModCrt((UInt128)modulus, cycles).Should().Be(expected);
}
private static ulong NaiveOrder(ulong q)
{
if (q <= 2UL)
{
return 0UL;
}
ulong order = 1UL;
ulong value = 2UL % q;
while (value != 1UL)
{
value = (value << 1) % q;
order++;
}
return order;
}
}