453 lines
16 KiB
C#
453 lines
16 KiB
C#
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace PerfectNumbers.Core.Cpu;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<AllowedMaskKey>
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to find a divisor q (within maxQBits) for M_p by streaming q via QNibbleGenerator16 and
|
|
/// validating with the deterministic planned-nibble automaton.
|
|
/// </summary>
|
|
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<int, Queue<State>>(capacity: 4096);
|
|
var termByBlock = new Dictionary<int, Queue<State>>(capacity: 4096);
|
|
var activeBlocks = new SortedSet<int>();
|
|
|
|
void Enqueue(State s)
|
|
{
|
|
var dict = s.KTerminated ? termByBlock : openByBlock;
|
|
if (!dict.TryGetValue(s.Block, out var q))
|
|
{
|
|
q = new Queue<State>();
|
|
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<AllowedMaskKey, ushort>(capacity: 1 << 16);
|
|
|
|
// Reusable buffers.
|
|
Span<byte> qNibByKn = stackalloc byte[16];
|
|
Span<MersenneCombinedDivisorScannerFullStreamExtended.QGenState16> nextByKn = stackalloc MersenneCombinedDivisorScannerFullStreamExtended.QGenState16[16];
|
|
Span<int> 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<State> 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);
|
|
}
|
|
}
|
|
}
|