diff --git a/PerfectNumbers.Core.Tests/MersenneNumberDivisorByDivisorTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberDivisorByDivisorTesterTests.cs new file mode 100644 index 0000000..42b4068 --- /dev/null +++ b/PerfectNumbers.Core.Tests/MersenneNumberDivisorByDivisorTesterTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using PerfectNumbers.Core; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +public class MersenneNumberDivisorByDivisorTesterTests +{ + [Fact] + [Trait("Category", "Fast")] + public void Run_marks_candidates_divisible_by_seven_or_eleven_as_composite() + { + var candidates = new List { 5UL, 7UL, 11UL, 13UL }; + var tester = new FakeTester(); + var results = new List<(ulong Prime, bool Searched, bool Detailed, bool Passed)>(); + int compositeMarks = 0; + int clearedMarks = 0; + + MersenneNumberDivisorByDivisorTester.Run( + candidates, + tester, + previousResults: null, + startPrime: 0UL, + markComposite: () => compositeMarks++, + clearComposite: () => clearedMarks++, + printResult: (prime, searched, detailed, passed) => results.Add((prime, searched, detailed, passed)), + threadCount: 1); + + tester.ConfiguredMaxPrime.Should().Be(13UL); + compositeMarks.Should().Be(2); + clearedMarks.Should().Be(2); + results.Should().Equal( + new[] + { + (5UL, true, true, true), + (7UL, true, true, false), + (11UL, true, true, false), + (13UL, true, true, true), + }); + } + + private sealed class FakeTester : IMersenneNumberDivisorByDivisorTester + { + public ulong ConfiguredMaxPrime { get; private set; } + + public int BatchSize { get; set; } + + public ulong DivisorLimit => ConfiguredMaxPrime + 100UL; + + public void ConfigureFromMaxPrime(ulong maxPrime) + { + ConfiguredMaxPrime = maxPrime; + } + + public ulong GetAllowedMaxDivisor(ulong prime) + { + return DivisorLimit; + } + + public bool IsPrime(ulong prime, out bool divisorsExhausted) + { + divisorsExhausted = true; + return prime % 7UL != 0UL && prime % 11UL != 0UL; + } + + public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession() + { + return new DummySession(); + } + + public void PrepareCandidates(ReadOnlySpan primes, Span allowedMaxValues) + { + for (int i = 0; i < primes.Length; i++) + { + allowedMaxValues[i] = 100UL; + } + } + + private sealed class DummySession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession + { + public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan primes, Span hits) + { + throw new NotSupportedException(); + } + + public void Dispose() + { + } + } + } +} diff --git a/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs index fdd85ea..a3a34a3 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs @@ -44,5 +44,41 @@ public class MersenneNumberDivisorCpuTesterTests session.CheckDivisor(31UL, MontgomeryDivisorData.FromModulus(31UL), cycle31, primes, hits); hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 }); } + + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite() + { + var tester = new MersenneNumberDivisorByDivisorCpuTester(); + tester.ConfigureFromMaxPrime(43UL); + + using var session = tester.CreateDivisorSession(); + ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL }; + byte[] hits = new byte[exponents.Length]; + + MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL); + ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData); + session.CheckDivisor(7UL, divisorData, cycle, exponents, hits); + + hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + } + + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite() + { + var tester = new MersenneNumberDivisorByDivisorCpuTester(); + tester.ConfigureFromMaxPrime(61UL); + + using var session = tester.CreateDivisorSession(); + ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL }; + byte[] hits = new byte[exponents.Length]; + + MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL); + ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData); + session.CheckDivisor(11UL, divisorData, cycle, exponents, hits); + + hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + } } diff --git a/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs index 3b58adc..3d36431 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using FluentAssertions; using PerfectNumbers.Core; @@ -180,6 +181,112 @@ public class MersenneNumberDivisorGpuTesterTests hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0, 0 }); } + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite() + { + var tester = new MersenneNumberDivisorByDivisorGpuTester + { + GpuBatchSize = 5, + }; + tester.ConfigureFromMaxPrime(43UL); + + using var session = tester.CreateDivisorSession(); + ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL }; + byte[] hits = new byte[exponents.Length]; + + MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL); + ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData); + + session.CheckDivisor(7UL, divisorData, cycle, exponents, hits); + + var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession) + .GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!; + ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!; + Span residues = hostBuffer.AsSpan(0, exponents.Length); + byte[] computedHits = new byte[exponents.Length]; + for (int i = 0; i < residues.Length; i++) + { + computedHits[i] = residues[i] == divisorData.MontgomeryOne ? (byte)1 : (byte)0; + } + + computedHits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + } + + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite() + { + var tester = new MersenneNumberDivisorByDivisorGpuTester + { + GpuBatchSize = 5, + }; + tester.ConfigureFromMaxPrime(61UL); + + using var session = tester.CreateDivisorSession(); + ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL }; + byte[] hits = new byte[exponents.Length]; + + MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL); + ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData); + + session.CheckDivisor(11UL, divisorData, cycle, exponents, hits); + + var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession) + .GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!; + ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!; + Span residues = hostBuffer.AsSpan(0, exponents.Length); + byte[] computedHits = new byte[exponents.Length]; + for (int i = 0; i < residues.Length; i++) + { + computedHits[i] = residues[i] == divisorData.MontgomeryOne ? (byte)1 : (byte)0; + } + + computedHits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + } + + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_gpu_tester_skips_divisors_excluded_by_small_cycle_generation() + { + var cycles = MersenneDivisorCycles.Shared; + var tableField = typeof(MersenneDivisorCycles).GetField("_table", BindingFlags.NonPublic | BindingFlags.Instance)!; + var smallCyclesField = typeof(MersenneDivisorCycles).GetField("_smallCycles", BindingFlags.NonPublic | BindingFlags.Instance)!; + + var originalTable = (List<(ulong Divisor, ulong Cycle)>)tableField.GetValue(cycles)!; + var originalSmall = (ulong[]?)smallCyclesField.GetValue(cycles); + + try + { + var patchedTable = new List<(ulong Divisor, ulong Cycle)>(); + ulong[] patchedSmall = new ulong[PerfectNumberConstants.MaxQForDivisorCycles + 1]; + + MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(191UL); + patchedSmall[191] = MersenneDivisorCycles.CalculateCycleLength(191UL, divisorData); + + tableField.SetValue(cycles, patchedTable); + smallCyclesField.SetValue(cycles, patchedSmall); + DivisorCycleCache.Shared.RefreshSnapshot(); + + var tester = new MersenneNumberDivisorByDivisorGpuTester(); + tester.ConfigureFromMaxPrime(19UL); + + typeof(MersenneNumberDivisorByDivisorGpuTester) + .GetField("_divisorLimit", BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(tester, 200UL); + + tester.IsPrime(19UL, out bool divisorsExhausted).Should().BeTrue(); + divisorsExhausted.Should().BeTrue(); + } + finally + { + tableField.SetValue(cycles, originalTable); + smallCyclesField.SetValue(cycles, originalSmall); + DivisorCycleCache.Shared.RefreshSnapshot(); + GpuContextPool.DisposeAll(); + } + } + [Fact] [Trait("Category", "Fast")] public void ByDivisor_cpu_session_reuses_cycle_remainders_across_primes()