using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; namespace PerfectNumbers.Core.Cpu; /// /// Block-ordered streaming divisor scan for M_p = 2^p - 1 in the family q = (2p)k + 1, /// using a 4-bit (base-16) residue automaton. /// /// Key design change vs column-wise branching: /// - We always assign an a-nibble (4 planned a-bits) before stepping the 4 columns. /// - With planned bits present, each of the 4 columns becomes deterministic (no 0/1 branching), /// so we can test each a-nibble in O(1) per column and cache allowed masks by signatures. /// /// This makes the "residue per block" perspective operational while staying exact for the explored tree. /// public static class MersenneResidueNibbleAutomatonScanner { public enum ScanResult { RuledOut = 0, Candidate = 1, FoundDivisor = 2 } public const int MaxSupportedQBits = 8192; // Cache allowed a-nibble masks only for moderate carry values. // For larger carry we compute directly to avoid any risk of false pruning. private const int CarryCacheMax = 511; // Reuse the same block-0 filter as the original scanner: q odd and q mod 8 in {1,7}. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsAllowedQ0Nibble(byte q0) { if ((q0 & 0x1) == 0) return false; int mod8 = q0 & 0x7; return mod8 == 0x1 || mod8 == 0x7; } private readonly struct AllowedMaskKey( int pBits, int maxQBits, int block, int carry, byte qNibble, ulong qSig, ulong aSig) : IEquatable { public readonly int PBits = pBits; public readonly int MaxQBits = maxQBits; public readonly int Block = block; public readonly int Carry = carry; public readonly byte QNibble = qNibble; public readonly ulong QSig = qSig; public readonly ulong ASig = aSig; public bool Equals(AllowedMaskKey other) => PBits == other.PBits && MaxQBits == other.MaxQBits && Block == other.Block && Carry == other.Carry && QNibble == other.QNibble && QSig == other.QSig && ASig == other.ASig; public override bool Equals(object? obj) => obj is AllowedMaskKey k && Equals(k); public override int GetHashCode() => HashCode.Combine(PBits, MaxQBits, Block, Carry, QNibble, QSig, ASig); } private struct State { public int Block; public int BitIndex; public int Carry; public bool KTerminated; public byte KFlushRemaining; public byte PostTermZeroQNibbles; public MersenneCombinedDivisorScannerFullStreamExtended.DynamicBitset QBits; public MersenneCombinedDivisorScannerFullStreamExtended.ABitsWindow AWindow; public MersenneCombinedDivisorScannerFullStreamExtended.QGenState16 QState; public State CloneShallow() => new() { Block = Block, BitIndex = BitIndex, Carry = Carry, KTerminated = KTerminated, KFlushRemaining = KFlushRemaining, PostTermZeroQNibbles = PostTermZeroQNibbles, QBits = QBits.CloneShared(), AWindow = AWindow.Clone(), QState = QState }; public static State CreateInitial(MersenneCombinedDivisorScannerFullStreamExtended.QGenState16 initQ, int maxQBits) { var s = new State { Block = initQ.NibbleIndex, BitIndex = 0, Carry = 0, QBits = new MersenneCombinedDivisorScannerFullStreamExtended.DynamicBitset(maxQBits), AWindow = new MersenneCombinedDivisorScannerFullStreamExtended.ABitsWindow(maxQBits), QState = initQ, KTerminated = false, KFlushRemaining = 0, PostTermZeroQNibbles = 0 }; s.QBits.Set(0); // q0 = 1 s.AWindow.SetPlanned(0, 1); // a0 = 1 return s; } } /// /// Try to find a divisor q (within maxQBits) for M_p by streaming q via QNibbleGenerator16 and /// validating with the deterministic planned-nibble automaton. /// public static ScanResult TryFindDivisorByResidueAutomaton( ulong prime, int maxQBits, bool verifyWithModPow, out BigInteger foundQ) { foundQ = BigInteger.Zero; if (prime == 0) return ScanResult.RuledOut; if (maxQBits < 2) throw new ArgumentOutOfRangeException(nameof(maxQBits)); if (maxQBits > MaxSupportedQBits) return ScanResult.Candidate; int pBits = checked((int)prime); int maxBlocks = (maxQBits + 3) / 4; var qGen = new MersenneCombinedDivisorScannerFullStreamExtended.QNibbleGenerator16(prime); // Block-ordered exploration (BFS by block index). var openByBlock = new Dictionary>(capacity: 4096); var termByBlock = new Dictionary>(capacity: 4096); var activeBlocks = new SortedSet(); void Enqueue(State s) { var dict = s.KTerminated ? termByBlock : openByBlock; if (!dict.TryGetValue(s.Block, out var q)) { q = new Queue(); dict[s.Block] = q; } q.Enqueue(s); activeBlocks.Add(s.Block); } bool TryDequeue(out State s) { s = default; if (activeBlocks.Count == 0) return false; int b = activeBlocks.Min; if (termByBlock.TryGetValue(b, out var tq) && tq.Count > 0) s = tq.Dequeue(); else if (openByBlock.TryGetValue(b, out var oq) && oq.Count > 0) s = oq.Dequeue(); else { activeBlocks.Remove(b); return false; } bool termEmpty = !termByBlock.TryGetValue(b, out var tq2) || tq2.Count == 0; bool openEmpty = !openByBlock.TryGetValue(b, out var oq2) || oq2.Count == 0; if (termEmpty && openEmpty) activeBlocks.Remove(b); return true; } // Cache of allowed a-nibble masks for (state signature, qNibble, carry). var allowedMaskCache = new Dictionary(capacity: 1 << 16); // Reusable buffers. Span qNibByKn = stackalloc byte[16]; Span nextByKn = stackalloc MersenneCombinedDivisorScannerFullStreamExtended.QGenState16[16]; Span countsByQn = stackalloc int[16]; var init = State.CreateInitial(MersenneCombinedDivisorScannerFullStreamExtended.QNibbleGenerator16.InitialState, maxQBits); Enqueue(init); while (TryDequeue(out var st)) { if (st.Block >= maxBlocks) { if (FinishScanToEndDeterministic(pBits, maxQBits, ref st) && st.Carry == 0) { BigInteger qVal = st.QBits.ToBigInteger(); if (qVal > BigInteger.One) { foundQ = qVal; if (verifyWithModPow) { bool ok = BigInteger.ModPow(2, prime, foundQ) == BigInteger.One; if (!ok) return ScanResult.Candidate; } return ScanResult.FoundDivisor; } } continue; } int startBit = st.Block << 2; // Reset buffers for (int i = 0; i < 16; i++) { qNibByKn[i] = 0xFF; nextByKn[i] = default; countsByQn[i] = 0; } if (st.KTerminated) { byte kn = 0; qGen.ComputeNext(st.QState, kn, out byte qn, out var ns); // Advance generator state and post-termination flushing st.QState = ns; if (st.KFlushRemaining > 0) st.KFlushRemaining--; ApplyQNibble(ref st, startBit, qn, maxQBits); if (qn == 0) st.PostTermZeroQNibbles++; else st.PostTermZeroQNibbles = 0; int requiredZeroRun = qGen.KWinLen + 1; if (st.KFlushRemaining == 0 && st.QState.Carry16 == 0 && st.PostTermZeroQNibbles >= requiredZeroRun) { // Freeze q and finish var done = st.CloneShallow(); done.Block = maxBlocks; Enqueue(done); continue; } qNibByKn[kn] = qn; nextByKn[kn] = ns; countsByQn[qn] = 1; } else { qGen.ComputeAllNext(st.QState, qNibByKn, nextByKn, countsByQn); } for (int qnVal = 0; qnVal < 16; qnVal++) { if (countsByQn[qnVal] == 0) continue; byte qn = (byte)qnVal; if (st.Block == 0 && !IsAllowedQ0Nibble(qn)) continue; // Compute allowed a-nibbles deterministically for this (state, qn). int sigEndBit = Math.Min(startBit + 3, pBits + maxQBits); ulong qSig = st.QBits.Signature64WithNibbleUpTo(sigEndBit, startBit, qn, maxQBits); ulong aSig = st.AWindow.Signature64(); ushort allowedMask; int carryForCache = st.Carry; if ((uint)carryForCache <= (uint)CarryCacheMax) { var key = new AllowedMaskKey(pBits, maxQBits, st.Block, carryForCache, qn, qSig, aSig); if (!allowedMaskCache.TryGetValue(key, out allowedMask)) { allowedMask = ComputeAllowedANibbleMaskDeterministic(pBits, maxQBits, st, startBit, qn); allowedMaskCache[key] = allowedMask; } } else { // Large carry: compute without caching. allowedMask = ComputeAllowedANibbleMaskDeterministic(pBits, maxQBits, st, startBit, qn); } if (allowedMask == 0) continue; // Expand k-nibbles mapping to this q-nibble. for (int kn = 0; kn < 16; kn++) { if (qNibByKn[kn] != qn) continue; if (st.Block == 0 && kn == 0) continue; // keep k>=1 (so q>1) var child = st.CloneShallow(); child.QState = nextByKn[kn]; bool canTerminateHere = !st.KTerminated && kn == 0 && st.Block > 0; ApplyQNibble(ref child, startBit, qn, maxQBits); // normal branch ExpandANibbles(pBits, maxQBits, maxQBits, ref child, startBit, allowedMask, Enqueue); // terminated branch if (canTerminateHere) { var termChild = st.CloneShallow(); termChild.QState = nextByKn[kn]; termChild.KTerminated = true; termChild.KFlushRemaining = (byte)qGen.KWinLen; termChild.PostTermZeroQNibbles = 0; ApplyQNibble(ref termChild, startBit, qn, maxQBits); ExpandANibbles(pBits, maxQBits, maxQBits, ref termChild, startBit, allowedMask, Enqueue); } } } } return ScanResult.RuledOut; } private static void ExpandANibbles( int pBits, int maxQBits, int windowBits, ref State baseState, int startBit, ushort allowedMask, Action push) { // Iterate allowed a-nibbles. for (int aNib = 0; aNib < 16; aNib++) { if ((allowedMask & (1 << aNib)) == 0) continue; if (startBit == 0 && (aNib & 1) == 0) continue; // enforce a0=1 var st = baseState.CloneShallow(); ApplyANibblePlanned(ref st, startBit, (byte)aNib); // Deterministically step the 4 columns (planned bits remove branching). if (!StepColumnsForBlockDeterministic(pBits, maxQBits, ref st, startBit)) continue; st.Block = st.QState.NibbleIndex; push(st); } } private static ushort ComputeAllowedANibbleMaskDeterministic(int pBits, int maxQBits, State st, int startBit, byte qNibble) { // Ensure qNibble is applied to the state for reading q bits within this block. var baseState = st.CloneShallow(); ApplyQNibble(ref baseState, startBit, qNibble, maxQBits); ushort mask = 0; for (int aNib = 0; aNib < 16; aNib++) { if (startBit == 0 && (aNib & 1) == 0) continue; var tmp = baseState.CloneShallow(); ApplyANibblePlanned(ref tmp, startBit, (byte)aNib); // Only test the 4 columns deterministically. if (StepColumnsForBlockDeterministic(pBits, maxQBits, ref tmp, startBit)) mask |= (ushort)(1 << aNib); } return mask; } private static bool StepColumnsForBlockDeterministic(int pBits, int maxQBits, ref State st, int startBit) { int endBit = Math.Min(startBit + 3, pBits + maxQBits); while (st.BitIndex <= endBit) { if (!StepSingleColumnDeterministic(pBits, maxQBits, ref st)) return false; st.BitIndex++; } return true; } private static bool FinishScanToEndDeterministic(int pBits, int maxQBits, ref State st) { int end = pBits + 1; while (st.BitIndex <= end) { int startBit = (st.BitIndex >> 2) << 2; if (!StepColumnsForBlockDeterministic(pBits, maxQBits, ref st, startBit)) return false; } return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool StepSingleColumnDeterministic(int pBits, int maxQBits, ref State st) { int j = st.BitIndex; int requiredBit = (j < pBits) ? 1 : 0; int sumOther = st.Carry; int maxT = Math.Min(j, maxQBits - 1); // Contributions from q_t=1 for t>0: add a_{j-t} foreach (int t in st.QBits.EnumerateSetBitsUpTo(maxT)) { if (t == 0) continue; int aIndex = j - t; if (aIndex < 0) continue; if (st.AWindow.GetBitRelative(aIndex) != 0) sumOther++; } // Deterministic a_j: must be planned or already committed; if missing, treat as unknown => not deterministic if (!st.AWindow.TryGetKnownBit(j, out int aJ)) return false; int sum = sumOther + aJ; if ((sum & 1) != requiredBit) return false; int newCarry = (requiredBit == 1) ? ((sum - 1) >> 1) : (sum >> 1); st.Carry = newCarry; // Commit bit into window (PushBit also clears planned for j). st.AWindow.PushBit(j, aJ); return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ApplyQNibble(ref State st, int startBit, byte qNibble, int maxQBits) { for (int b = 0; b < 4; b++) { int bitPos = startBit + b; if (bitPos >= maxQBits) continue; int bit = (qNibble >> b) & 1; if (bit != 0) st.QBits.Set(bitPos); } st.QBits.Set(0); // enforce q0=1 } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ApplyANibblePlanned(ref State st, int startBit, byte aNibble) { for (int b = 0; b < 4; b++) { int bitPos = startBit + b; int bit = (aNibble >> b) & 1; st.AWindow.SetPlanned(bitPos, bit); } } }