Files
even-perfect-numbers-scanner/PerfectNumbers.Core/Cpu/MersenneCombinedDivisorScanner.cs
T

1988 lines
59 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core.Cpu;
/// <summary>
/// Deterministic streaming (column-wise) reconstruction scan for M_p = 2^p - 1,
/// exploring divisors in the family q = (2p)*k + 1 without materializing full q during search.
///
/// IMPORTANT:
/// - This scanner is exact for the explored (q,k) search tree up to maxQBits.
/// - However, exploring "all possible" k for very large maxQBits is combinatorially explosive.
/// The intent is to use strong pruning to rule out branches early.
/// - For practical use, keep maxQBits in a regime you can actually explore (hundreds or a few thousands),
/// and rely on pruning/caches.
/// </summary>
public static class MersenneCombinedDivisorScannerFullStreamExtended
{
// ===== DEBUG SETTINGS =====
private static readonly bool DebugEnabled = true;
private static readonly BigInteger DebugTargetQ =
BigInteger.Parse("1209708008767");
private const int DebugMaxBlocks = 20; // wystarczy (q ma ~11 bloków)
private static bool DebugPrefixMatches(BigInteger qNow, int producedNibbles)
{
int producedBits = producedNibbles * 4;
// target ma ~41 bitów; powyżej nie ma sensu maskować dalej
if (producedBits >= 128) producedBits = 128; // bezpiecznie, debug tylko
BigInteger mask = (BigInteger.One << producedBits) - BigInteger.One;
return (qNow & mask) == (DebugTargetQ & mask);
}
private const int MaxColumnChoices = 2;
public enum ScanResult
{
RuledOut = 0,
Candidate = 1,
FoundDivisor = MaxColumnChoices
}
// Hard safety ceiling for in-memory bitsets/windows in this implementation.
// You can raise it, but exploration cost grows extremely fast with maxQBits.
public const int MaxSupportedQBits = 8192;
// Block-0 filter: q is odd and q ≡ 1 or 7 (mod 8).
// NOTE: Do NOT apply any “not divisible by 5” filter based on low bits; it is not valid in base-2 local form.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedQ0Nibble(byte q0)
{
// odd
if ((q0 & 0x1) == 0) return false;
// q mod 8 in {1,7}
int mod8 = q0 & 0x7;
return mod8 == 0x1 || mod8 == 0x7;
}
/// <summary>
/// Deterministic streaming reconstruction scan for M_p = 2^p - 1.
///
/// The scan explores q in the family q = (2p)*k + 1, where k is an unbounded nonnegative integer,
/// but truncates q to maxQBits. The scan maintains a rolling window of a-bits of size maxQBits,
/// sufficient to evaluate the convolution exactly for all columns j up to p+maxQBits.
///
/// Return values:
/// - FoundDivisor: found a q within maxQBits such that the reconstructed a satisfies a*q = 2^p - 1 (in all columns).
/// - RuledOut: no such q exists within the explored tree and limits.
/// - Candidate: scan did not find a divisor within the explored q-bit budget (maxQBits),
/// or maxQBits exceeds MaxSupportedQBits.
///
/// Notes:
/// - This is an exact column-wise decision procedure for each explored branch, but exploration is exponential in maxQBits.
/// - Optional ModPow verification is performed only when FoundDivisor is reached.
/// </summary>
public static ScanResult TryFindDivisorByStreamingScan(
ulong prime,
int maxQBits,
bool verifyWithModPow,
out BigInteger foundQ)
{
foundQ = BigInteger.Zero;
if (prime == 0) return ScanResult.RuledOut;
ArgumentOutOfRangeException.ThrowIfLessThan(maxQBits, MaxColumnChoices);
if (maxQBits > MaxSupportedQBits) return ScanResult.Candidate;
int pBits = checked((int)prime);
int maxBlocks = (maxQBits + 3) / 4;
var qGen = new QNibbleGenerator16(prime);
// Layered (block-ordered) exploration: always exhaust smaller block indices first.
// This is important for correctness of conservative block-level pruning and for predictable progress.
var openByBlock = new Dictionary<int, Queue<State>>(capacity: 4096);
var termByBlock = new Dictionary<int, Queue<State>>(capacity: 4096);
var activeBlocks = new SortedSet<int>();
// Per-block conservative global pruning stats (computed over the set of *actually reachable* reduced states at that block).
var blockPrune = new Dictionary<int, BlockPruneStats>(capacity: 1024);
// Per-state allowed-a cache (must remain state-sensitive; see AllowedMaskKey).
var allowedMaskCache = new Dictionary<AllowedMaskKey, ushort>(capacity: 1 << 16);
void EnqueueState(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 TryDequeueState(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
{
// Should not happen, but keep structure consistent.
activeBlocks.Remove(b);
return false;
}
// Cleanup active block if both queues are empty.
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;
}
var init = State.CreateInitial(QNibbleGenerator16.InitialState, maxQBits);
EnqueueState(init);
Span<int> counts = stackalloc int[16];
Span<byte> qNib = stackalloc byte[16];
Span<QGenState16> nextState = stackalloc QGenState16[16];
Span<byte> aOrder = stackalloc byte[16]; // reusable buffer for nibble ordering (avoid stackalloc in loops)
int lastLoggedBlock = -1;
State st;
while (TryDequeueState(out st))
{
// Intentionally no internal max-nodes limit.
if (DebugEnabled && st.Block != lastLoggedBlock)
{
lastLoggedBlock = st.Block;
Console.WriteLine($"[PROGRESS] p={prime}, reached block={st.Block}");
}
// If we have finished producing all q nibbles (to maxQBits), finish scanning remaining columns.
if (st.Block >= maxBlocks)
{
if (FinishScanToEnd(pBits, maxQBits, ref st) && st.Carry == 0)
{
// Found a fully consistent reconstruction for this branch.
BigInteger qVal = st.QBits.ToBigInteger();
if (qVal > BigInteger.One)
{
foundQ = qVal;
Console.WriteLine($"[stream-scan] Found divisor candidate q(bits<={maxQBits}) for p={prime}");
if (verifyWithModPow)
{
bool ok = BigInteger.ModPow(MaxColumnChoices, prime, foundQ) == BigInteger.One;
Console.WriteLine($"[stream-scan] ModPow verification: {(ok ? "OK" : "FAILED")}");
}
return ScanResult.FoundDivisor;
}
}
continue;
}
int startBit = st.Block << 2;
// Enumerate possible k-nibbles for this q-nibble; group by qNibble to avoid recomputation
// of allowed a choices (which depend on qNibble and carry).
// Reset tables for this state.
qNib.Clear();
nextState.Clear();
counts.Clear();
// byte forcedKn;
// int ni = st.QState.NibbleIndex;
// if (ni == 0) forcedKn = 0xF;
// else if (ni == 1) forcedKn = 0x1;
// else if (ni == 2) forcedKn = 0x1;
// else if (ni == 3) forcedKn = 0x1;
// else forcedKn = 0x0;
// for (byte kn = 0; kn <= 15; kn++)
// {
// if (kn != forcedKn) continue;
// for (int i = 0; i < 16; i++) qNib[i] = 0xFF;
// for (int i = 0; i < 16; i++) nextState[i] = default; // opcjonalnie
// qGen.ComputeNext(st.QState, forcedKn, out byte qn, out QGenState16 ns);
// qNib[forcedKn] = qn;
// nextState[forcedKn] = ns;
// counts[qn] = 1;
// }
// Sentinel init: mark unused kn entries as invalid (0xFF) to avoid accidental matches when qn==0.
for (int i = 0; i < 16; i++) qNib[i] = 0xFF;
// nextState default is fine; we will only read it when qNib[kn] == qn (so it was written).
for (int i = 0; i < 16; i++) nextState[i] = default;
// Jeśli k już zakończone, jedyny dozwolony kn=0
if (st.KTerminated)
{
byte kn = 0;
qGen.ComputeNext(st.QState, kn, out byte qn, out QGenState16 ns);
// Aktualizuj stan generatora
st.QState = ns;
// Flush okna k: po terminacji trzeba wykonać dokładnie KWinLen kroków kn=0,
// żeby wszystkie historyczne nibble k przestały wpływać na q.
if (st.KFlushRemaining > 0)
st.KFlushRemaining--;
// Dopisz q nibble do bitsetu q
ApplyQNibble(ref st, startBit, qn, maxQBits);
// After k termination we keep streaming q with kn=0.
// We can only declare "end of q" once the k-window is flushed AND we observe a stable run of zero q-nibbles.
// This avoids premature freezing when a later carry/window interaction could still produce a non-zero nibble.
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)
{
Console.WriteLine($"[QFROZEN] p={prime} at block={st.Block} nib={st.QState.NibbleIndex} carry16={st.QState.Carry16} zeroRun={st.PostTermZeroQNibbles}");
// Jump to final phase (FinishScanToEnd) without materializing full q.
var done = st.CloneShallow();
done.Block = maxBlocks;
EnqueueState(done);
continue;
}
qNib[kn] = qn;
nextState[kn] = ns;
counts[qn] = 1;
}
else
{
// Enumerate all possible k-nibbles at this position (optimized: compute base once per state).
qGen.ComputeAllNext(st.QState, qNib, nextState, counts);
// Dodatkowa gałąź: “zakończ k TERAZ” (czyli od tego miejsca kn=0 zawsze).
// Realizujemy to poprzez dopuszczenie kn=0, ale z flagą KTerminated w dziecku.
// Nie musisz robić dodatkowego liczenia counts; obsłużymy to przy tworzeniu child.
}
// Conservative per-block pruning:
// Track which q-nibbles are impossible for *all reachable reduced states* at this block.
// This is safe because we explore blocks in increasing order; once we move past a block, no new states for it appear.
ulong qPrefixSig = st.QBits.Signature64UpTo(startBit - 1);
ulong aSigNow = st.AWindow.Signature64();
var reducedKey = new ReducedStateKey(pBits, maxQBits, st.Block, st.Carry, qPrefixSig, aSigNow);
if (!blockPrune.TryGetValue(st.Block, out var pruneStats))
{
pruneStats = new BlockPruneStats();
blockPrune[st.Block] = pruneStats;
}
pruneStats.RegisterState(reducedKey);
for (int qnVal = 0; qnVal < 16; qnVal++)
{
if (counts[qnVal] == 0) continue;
byte qn = (byte)qnVal;
if (st.Block == 0 && !IsAllowedQ0Nibble(qn))
continue;
// Global (block-level) forbidden q-nibble mask, conservatively derived from all reachable reduced states.
if ((pruneStats.GlobalForbiddenMask & (1 << qn)) != 0)
continue;
// Early contradiction pruning (interval-based carry propagation), inspired by BitContradictionSolver.
// Important: bits of 'a' outside the window are treated as Unknown (never as 0), so this pruning cannot
// introduce false negatives.
if (QuickRejectQNibbleByBounds(pBits, maxQBits, in st, startBit, qn))
{
pruneStats.MarkRejected(reducedKey, qn);
continue;
}
// Additional early pruning: derive a conservative candidate mask for the 4-bit a-nibble at this block
// by checking (using the same bounds logic) whether each a_j in this block can be 0/1.
// If a bit is forced (only one value feasible), we restrict the candidate a-nibble mask.
// We also derive a conservative *preference* mask/value for branch ordering (no pruning).
// Bits outside the a-window are treated as Unknown, so this cannot introduce false negatives.
var boundsInfo = ComputeCandidateANibbleMaskAndPreferencesByBounds(pBits, maxQBits, in st, startBit, qn);
ushort preMask = boundsInfo.CandidateMask;
if (preMask == 0)
{
pruneStats.MarkRejected(reducedKey, qn);
continue;
}
// Precompute which a-nibbles are compatible with this q-nibble and current carry,
// using LUT for delta contribution plus exact base via the rolling a-window and current q-bitset.
int sigEndBit = Math.Min(startBit + 3, pBits + maxQBits);
ulong qSig = st.QBits.Signature64WithNibbleUpTo(sigEndBit, startBit, qn, maxQBits);
ulong aSig = st.AWindow.Signature64();
var key = new AllowedMaskKey(pBits, maxQBits, st.Block, st.Carry, qn, qSig, aSig, preMask);
if (!allowedMaskCache.TryGetValue(key, out ushort allowedAMask))
{
allowedAMask = ComputeAllowedANibbleMaskFiltered(pBits, maxQBits, ref st, startBit, qn, preMask);
allowedMaskCache[key] = allowedAMask;
}
if (allowedAMask == 0 && DebugEnabled && st.QState.NibbleIndex <= DebugMaxBlocks)
{
var tmp = st.CloneShallow();
ApplyQNibble(ref tmp, startBit, qn, maxQBits);
BigInteger qTmp = tmp.QBits.ToBigInteger();
int produced = st.QState.NibbleIndex + 1;
if (DebugPrefixMatches(qTmp, produced))
Console.WriteLine($"[DEBUG CUT-A-PREFIX] block={produced} qLow={qTmp} qNib=0x{qn:X}");
}
if (allowedAMask == 0)
{
pruneStats.MarkRejected(reducedKey, qn);
continue;
}
// Expand all k-nibbles mapping to this q-nibble.
// We will apply branch ordering to a-nibbles using bounds-derived preferences (no pruning).
byte prefMask = boundsInfo.PrefMask;
byte prefValue = boundsInfo.PrefValue;
for (int kn = 0; kn < 16; kn++)
{
if (qNib[kn] != qn) continue;
if (st.Block == 0 && kn == 0) continue;
// if (DebugEnabled && kn == 0 && qNib[kn] == qn)
// {
// Console.WriteLine($"[KN0] p={prime} block={st.Block} qn=0x{qn:X} canTerminateHere={!st.KTerminated}");
// }
var child = st.CloneShallow(); // share arrays via copy-on-write where safe (implemented as cloning bitsets)
child.QState = nextState[kn];
// Jeśli nie jesteśmy w trybie terminated i wybraliśmy kn==0,
// to rozgałęź na dwa warianty:
// - zwykły: k nadal może mieć wyższe nible,
// - terminated: uznajemy, że k kończy się tutaj.
bool canTerminateHere = !st.KTerminated && kn == 0 && st.Block > 0;
child.KTerminated = st.KTerminated; // default
// wariant 1: zwykły (nie terminujemy)
ApplyQNibble(ref child, startBit, qn, maxQBits);
// if (DebugEnabled && child.QState.NibbleIndex <= DebugMaxBlocks)
// {
// BigInteger qNow = child.QBits.ToBigInteger();
// if (DebugPrefixMatches(qNow, child.QState.NibbleIndex))
// {
// Console.WriteLine($"[DEBUG HIT-Q-ONLY-PREFIX] block={child.QState.NibbleIndex} qLow={qNow}");
// }
// }
// ===== wariant 1: normalnie kontynuujemy (KTerminated=false) =====
int aCount = BuildOrderedNibbleList(allowedAMask, prefMask, prefValue, aOrder);
for (int ai = 0; ai < aCount; ai++)
{
int aNib = aOrder[ai];
var grand = child.CloneShallow();
ApplyANibble(ref grand, startBit, (byte)aNib);
foreach (var branched in StepColumnsForBlockMulti(pBits, maxQBits, grand, startBit))
{
var pushState = branched;
pushState.Block = pushState.QState.NibbleIndex;
EnqueueState(pushState);
}
}
// wariant 2: terminujemy k (tylko jeśli kn==0 i jeszcze nie terminated)
if (canTerminateHere)
{
var termChild = st.CloneShallow();
termChild.QState = nextState[kn];
termChild.KFlushRemaining = (byte)qGen.KWinLen;
termChild.PostTermZeroQNibbles = 0;
termChild.KTerminated = true;
// Console.WriteLine($"[KTERM] p={prime} creating terminated branch at block={st.Block}, nib={st.QState.NibbleIndex}");
ApplyQNibble(ref termChild, startBit, qn, maxQBits);
// kontynuujemy stan z terminacją k
int aCount2 = BuildOrderedNibbleList(allowedAMask, prefMask, prefValue, aOrder);
for (int ai2 = 0; ai2 < aCount2; ai2++)
{
int aNib = aOrder[ai2];
var grand = termChild.CloneShallow();
ApplyANibble(ref grand, startBit, (byte)aNib);
foreach (var branched in StepColumnsForBlockMulti(pBits, maxQBits, grand, startBit))
{
var pushState = branched;
pushState.Block = pushState.QState.NibbleIndex;
EnqueueState(pushState);
}
}
}
}
}
}
return ScanResult.RuledOut;
}
private static ushort ComputeAllowedANibbleMask(int pBits, int maxQBits, ref State st, int startBit, byte qNibble)
{
return ComputeAllowedANibbleMaskFiltered(pBits, maxQBits, ref st, startBit, qNibble, 0xFFFF);
}
private static ushort ComputeAllowedANibbleMaskFiltered(int pBits, int maxQBits, ref State st, int startBit, byte qNibble, ushort candidateMask)
{
// Correctness-first implementation:
// - never mutate the caller state
// - pre-apply qNibble once to improve performance (it is the same for all aNib candidates)
ushort mask = 0;
var baseState = st.CloneShallow();
ApplyQNibble(ref baseState, startBit, qNibble, maxQBits);
for (int aNib = 0; aNib < 16; aNib++)
{
if ((candidateMask & (1 << aNib)) == 0) continue;
// In block0, enforce a0 bit0 = 1 (since a0=1).
if (startBit == 0 && (aNib & 1) == 0) continue;
var tmp = baseState.CloneShallow();
ApplyANibble(ref tmp, startBit, (byte)aNib);
bool ok = false;
foreach (var _ in StepColumnsForBlockMulti(pBits, maxQBits, tmp, startBit))
{
ok = true;
break;
}
if (ok) mask |= (ushort)(1 << aNib);
}
return mask;
}
// ------------------------------------------------------------------------
// Early a-nibble candidate mask derivation (forced bits) via bounds
// ------------------------------------------------------------------------
// This uses the same conservative parity/bounds logic as QuickRejectQNibbleByBounds, but instead of only
// rejecting qNibble, it attempts to determine whether each a_j in this 4-column block can be 0/1.
// If exactly one of {0,1} is feasible for a_j (under the conservative bounds model), that bit is treated as forced
// and used to restrict the candidate a-nibble space.
//
// IMPORTANT: This MUST be conservative. It may fail to detect some forced bits (false negatives),
// but it must never rule out a real solution (no false positives on forcing). Our feasibility checks use
// an over-approximate interval [minSum,maxSum] and therefore only declare "impossible" when parity cannot
// be achieved anywhere in that interval.
private readonly struct BoundsANibbleInfo
{
public readonly ushort CandidateMask;
public readonly byte PrefMask; // bits within the 4-bit nibble for which we have a preference
public readonly byte PrefValue; // preferred bit values for those positions
public BoundsANibbleInfo(ushort candidateMask, byte prefMask, byte prefValue)
{
CandidateMask = candidateMask;
PrefMask = prefMask;
PrefValue = prefValue;
}
}
private static BoundsANibbleInfo ComputeCandidateANibbleMaskAndPreferencesByBounds(int pBits, int maxQBits, in State st, int startBit, byte qNibble)
{
// Start with all 16 nibbles allowed.
ushort mask = 0xFFFF;
byte prefMask = 0;
byte prefValue = 0;
long carryMin = st.Carry;
long carryMax = st.Carry;
for (int j = startBit; j <= startBit + 3; j++)
{
if (j > pBits + maxQBits) break;
int requiredBit = (j < pBits) ? 1 : 0;
// Compute forced/possible contributions from offsets t>0 (excluding a_j itself).
long forcedOther = 0;
long possibleOther = 0;
int maxT = Math.Min(j, maxQBits - 1);
for (int t = 1; t <= maxT; t++)
{
if (GetQBitWithNibble(in st, startBit, qNibble, t, maxQBits) == 0) continue;
int aIndex = j - t;
if (aIndex < 0) continue;
if (st.AWindow.TryGetKnownBit(aIndex, out int abit))
{
if (abit != 0)
{
forcedOther++;
possibleOther++;
}
}
else
{
possibleOther++;
}
}
bool ajKnown = st.AWindow.TryGetKnownBit(j, out int ajBit);
bool can0, can1;
if (ajKnown)
{
can0 = ajBit == 0 && IsParityReachableForCarryRange(carryMin, carryMax, forcedOther, possibleOther, 0, requiredBit);
can1 = ajBit == 1 && IsParityReachableForCarryRange(carryMin, carryMax, forcedOther, possibleOther, 1, requiredBit);
}
else
{
can0 = IsParityReachableForCarryRange(carryMin, carryMax, forcedOther, possibleOther, 0, requiredBit);
can1 = IsParityReachableForCarryRange(carryMin, carryMax, forcedOther, possibleOther, 1, requiredBit);
}
if (!can0 && !can1)
return new BoundsANibbleInfo(0, 0, 0);
// If only one value is feasible (under our conservative model), restrict the nibble mask.
int bitInNibble = j - startBit;
if (bitInNibble >= 0 && bitInNibble < 4)
{
if (!can0 && can1)
{
mask &= (ushort)ForceNibbleBit(bitInNibble, 1);
prefMask |= (byte)(1 << bitInNibble);
prefValue |= (byte)(1 << bitInNibble);
}
else if (can0 && !can1)
{
mask &= (ushort)ForceNibbleBit(bitInNibble, 0);
prefMask |= (byte)(1 << bitInNibble);
prefValue &= (byte)~(1 << bitInNibble);
}
else
{
// Both possible: derive a *preference* for branch ordering.
// Prefer the value that yields a narrower next carry range under the same conservative model.
long n0Min, n0Max, n1Min, n1Max;
bool ok0 = TryPropagateCarryBounds(carryMin, carryMax, forcedOther + 0, possibleOther + 0, requiredBit, out n0Min, out n0Max);
bool ok1 = TryPropagateCarryBounds(carryMin, carryMax, forcedOther + 1, possibleOther + 1, requiredBit, out n1Min, out n1Max);
// ok0/ok1 should match can0/can1, but keep defensive.
if (ok0 && ok1)
{
long w0 = n0Max - n0Min;
long w1 = n1Max - n1Min;
int prefer = (w1 < w0) ? 1 : (w0 < w1 ? 0 : 0);
prefMask |= (byte)(1 << bitInNibble);
if (prefer == 1) prefValue |= (byte)(1 << bitInNibble);
else prefValue &= (byte)~(1 << bitInNibble);
}
}
}
// Update carry range for next column using *unknown* a_j if not known and not forced.
// For safety, we propagate using the full [min,max] interval that covers all feasible choices of a_j.
long forced = forcedOther;
long possible = possibleOther;
if (ajKnown)
{
if (ajBit != 0) { forced++; possible++; }
}
else
{
// if both 0 and 1 are feasible, treat as unknown -> possible++
// if forced, we can reflect it in forced/possible.
if (!can0 && can1) { forced++; possible++; }
else if (can0 && !can1) { /* forced 0 -> nothing */ }
else { possible++; }
}
if (!TryPropagateCarryBounds(carryMin, carryMax, forced, possible, requiredBit, out long nextMin, out long nextMax))
return new BoundsANibbleInfo(0, 0, 0);
carryMin = nextMin;
carryMax = nextMax;
}
return new BoundsANibbleInfo(mask, prefMask, prefValue);
}
private static ushort ComputeCandidateANibbleMaskByBounds(int pBits, int maxQBits, in State st, int startBit, byte qNibble)
=> ComputeCandidateANibbleMaskAndPreferencesByBounds(pBits, maxQBits, in st, startBit, qNibble).CandidateMask;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsParityReachableForCarryRange(long carryMin, long carryMax, long forcedOther, long possibleOther, int aJ, int requiredBit)
{
int parity = requiredBit & 1;
long minSum = carryMin + forcedOther + aJ;
long maxSum = carryMax + possibleOther + aJ;
long minAligned = AlignUpToParity(minSum, parity);
long maxAligned = AlignDownToParity(maxSum, parity);
return minAligned <= maxAligned;
}
// Returns a 16-bit mask allowing only those a-nibbles whose bit 'b' equals requiredValue.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ForceNibbleBit(int b, int requiredValue)
{
int mask = 0;
for (int n = 0; n < 16; n++)
{
if (((n >> b) & 1) == requiredValue)
mask |= 1 << n;
}
return mask;
}
// Build an ordered list of a-nibbles from a bitmask, using bounds-derived preferences for branch ordering.
// This never prunes: it only reorders exploration to reach contradictions / valid branches earlier.
//
// Scoring: prefer nibbles matching preferred bit-values on positions indicated by prefMask.
private static int BuildOrderedNibbleList(ushort allowedMask, byte prefMask, byte prefValue, Span<byte> outList)
{
int count = 0;
for (int n = 0; n < 16; n++)
{
if ((allowedMask & (1 << n)) == 0) continue;
outList[count++] = (byte)n;
}
if (count <= 1 || prefMask == 0)
return count;
// Small selection sort (count <= 16).
for (int i = 0; i < count - 1; i++)
{
int best = i;
int bestScore = ScoreNibble(outList[i], prefMask, prefValue);
for (int j = i + 1; j < count; j++)
{
int s = ScoreNibble(outList[j], prefMask, prefValue);
if (s > bestScore)
{
best = j;
bestScore = s;
}
}
if (best != i)
{
(byte a, byte b) = (outList[i], outList[best]);
outList[i] = b;
outList[best] = a;
}
}
return count;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ScoreNibble(byte nib, byte prefMask, byte prefValue)
{
uint matches = (uint)(~(nib ^ prefValue) & prefMask) & 0xFu;
return (int)BitOperations.PopCount(matches);
}
// ------------------------------------------------------------------------
// Early contradiction pruning (interval-based carry propagation)
// ------------------------------------------------------------------------
// This is a conservative pre-filter inspired by BitContradictionSolver.TryPropagateCarry.
// We do NOT commit any new a-bits here. We only prove that a given (state, qNibble) cannot
// satisfy the required parity constraints in any of the next 4 columns, even if all currently
// unknown a-bits were chosen adversarially.
//
// Key correctness constraint (per user request): a-bits outside the window are treated as UNKNOWN,
// never as 0. Therefore this pruning cannot eliminate a valid solution branch.
private static bool QuickRejectQNibbleByBounds(int pBits, int maxQBits, in State st, int startBit, byte qNibble)
{
long carryMin = st.Carry;
long carryMax = st.Carry;
for (int j = startBit; j <= startBit + 3; j++)
{
if (j > pBits + maxQBits) break;
int requiredBit = (j < pBits) ? 1 : 0;
// Compute forced/possible ones contributed to column j excluding carry.
// We model: sum = carry + a_j + Σ_{t>0, q_t=1} a_{j-t}
long forced = 0;
long possible = 0;
int maxT = Math.Min(j, maxQBits - 1);
for (int t = 1; t <= maxT; t++)
{
if (GetQBitWithNibble(in st, startBit, qNibble, t, maxQBits) == 0) continue;
int aIndex = j - t;
if (aIndex < 0) continue;
if (st.AWindow.TryGetKnownBit(aIndex, out int abit))
{
if (abit != 0)
{
forced++;
possible++;
}
}
else
{
// Unknown a-bit outside window or not yet assigned.
possible++;
}
}
// Include a_j itself.
if (st.AWindow.TryGetKnownBit(j, out int aJKnown))
{
if (aJKnown != 0)
{
forced++;
possible++;
}
}
else
{
possible++;
}
if (!TryPropagateCarryBounds(carryMin, carryMax, forced, possible, requiredBit, out long nextMin, out long nextMax))
return true;
carryMin = nextMin;
carryMax = nextMax;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetQBitWithNibble(in State st, int startBit, byte qNibble, int bitPos, int maxQBits)
{
if (bitPos == 0) return 1; // enforce q0=1 always
if ((uint)bitPos >= (uint)maxQBits) return 0;
int baseBit = st.QBits.Get(bitPos);
if (bitPos >= startBit && bitPos < startBit + 4)
{
int b = bitPos - startBit;
int nibBit = (qNibble >> b) & 1;
return baseBit | nibBit;
}
return baseBit;
}
private static bool TryPropagateCarryBounds(long carryMin, long carryMax, long forcedOnes, long possibleOnes, int requiredBit,
out long nextCarryMin, out long nextCarryMax)
{
// Equivalent in spirit to BitContradictionSolver.TryPropagateCarry, but we track a carry *range*
// and return the union of feasible next carry values.
int parity = requiredBit & 1;
nextCarryMin = long.MaxValue;
nextCarryMax = long.MinValue;
for (long carry = carryMin; carry <= carryMax; carry++)
{
long minSum = carry + forcedOnes;
long maxSum = carry + possibleOnes;
long minAligned = AlignUpToParity(minSum, parity);
long maxAligned = AlignDownToParity(maxSum, parity);
if (minAligned > maxAligned) continue;
long cMin = (minAligned - requiredBit) >> 1;
long cMax = (maxAligned - requiredBit) >> 1;
if (cMin < nextCarryMin) nextCarryMin = cMin;
if (cMax > nextCarryMax) nextCarryMax = cMax;
}
if (nextCarryMin == long.MaxValue)
{
nextCarryMin = 0;
nextCarryMax = 0;
return false;
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static long AlignUpToParity(long value, int parity)
=> ((value & 1) == parity) ? value : value + 1;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static long AlignDownToParity(long value, int parity)
=> ((value & 1) == parity) ? value : value - 1;
private static bool StepColumnsForBlock(int pBits, int maxQBits, ref State st, int startBit)
{
int endBit = Math.Min(startBit + 3, pBits + maxQBits);
while (st.BitIndex <= endBit)
{
if (!StepSingleColumn(pBits, maxQBits, ref st))
return false;
st.BitIndex++;
}
return true;
}
private static IEnumerable<State> StepColumnsForBlockMulti(int pBits, int maxQBits, State seed, int startBit)
{
int endBit = Math.Min(startBit + 3, pBits + maxQBits);
// Lokalny work-stack: eksplorujemy tylko 4 kolumny.
// Maksymalnie 2^4 = 16 stanów na seed (zwykle znacznie mniej).
var work = new FixedCapacityStack<State>(capacity: 32);
work.Push(seed);
int[] choices = new int[MaxColumnChoices];
while (work.Count > 0)
{
var st = work.Pop();
// Jeśli przeszliśmy już cały blok, zwracamy stan jako wynik.
if (st.BitIndex > endBit)
{
yield return st;
continue;
}
int j = st.BitIndex;
int requiredBit = (j < pBits) ? 1 : 0;
int sumOther = st.Carry;
int maxT = Math.Min(j, maxQBits - 1);
// policz wkład z q_t=1 (t>0): add a_{j-t}
// (bez capture ref – używamy lokalnych)
var qBits = st.QBits;
var aWin = st.AWindow;
foreach (int t in qBits.EnumerateSetBitsUpTo(maxT))
{
if (t == 0) continue;
int aIndex = j - t;
if (aIndex < 0) continue;
if (aWin.GetBitRelative(aIndex) != 0)
sumOther++;
}
// planned?
bool hasPlanned = aWin.TryGetPlanned(j, out int plannedAJ);
// Policz, które aJ są dopuszczalne dla tej kolumny
int choiceCount = 0;
for (int aJ = 0; aJ <= 1; aJ++)
{
if (hasPlanned && aJ != plannedAJ) continue;
int sum = sumOther + aJ;
if ((sum & 1) != requiredBit)
continue;
choices[choiceCount++] = aJ;
}
if (choiceCount == 0)
{
// dead end for this local state
continue;
}
// Dla każdej dopuszczalnej wartości aJ twórz stan następny.
// Pierwszy wariant modyfikujemy "in place", kolejne klonujemy.
ScheduleState(work, choices[0], j, requiredBit, sumOther, st);
if (choiceCount == 2)
{
ScheduleState(work, choices[1], j, requiredBit, sumOther, st.CloneShallow());
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static void ScheduleState(FixedCapacityStack<State> work, int aJ, int j, int requiredBit, int sumOther, State next)
{
int sum = sumOther + aJ;
int nextCarry = (requiredBit == 1) ? ((sum - 1) >> 1) : (sum >> 1);
next.Carry = nextCarry;
next.AWindow.PushBit(j, aJ);
next.BitIndex = j + 1;
work.Push(next);
}
private static bool FinishScanToEnd(int pBits, int maxQBits, ref State st)
{
int end = pBits + 1;
while (st.BitIndex <= end)
{
int startBit = (st.BitIndex >> MaxColumnChoices) << MaxColumnChoices;
bool advanced = false;
foreach (var next in StepColumnsForBlockMulti(pBits, maxQBits, st, startBit))
{
st = next;
advanced = true;
break; // deterministycznie wybieramy jedną spójną ścieżkę
}
if (!advanced)
return false;
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool StepSingleColumn(int pBits, int maxQBits, ref State st)
{
int j = st.BitIndex;
int requiredBit = (j < pBits) ? 1 : 0;
int sumOther = st.Carry;
// contributions from q_t=1 for t>0: add a_{j-t}
// iterate over set bits of q (excluding t=0)
int maxT = Math.Min(j, maxQBits - 1);
// NOTE: 'st' is a ref parameter; do not capture it in a lambda (CS1628).
// Copy required members to locals.
ABitsWindow aWindow = st.AWindow;
int jj = j;
int baseJ = aWindow.CurrentJ; // last committed column (typically j-1 while processing column j)
foreach (int t in st.QBits.EnumerateSetBitsUpTo(maxT))
{
if (t == 0) continue;
int aIndex = jj - t;
if (aIndex < 0) continue;
if (aWindow.GetBitRelative(aIndex) != 0)
sumOther++;
}
// decide a_j by parity, because q0=1 is always set.
bool anyOk = false;
int newCarry = 0;
bool hasPlanned = st.AWindow.TryGetPlanned(j, out int plannedAJ);
for (int aJ = 0; aJ <= 1; aJ++)
{
if (hasPlanned && aJ != plannedAJ) continue;
int sum = sumOther + aJ;
if ((sum & 1) != requiredBit)
continue;
newCarry = (requiredBit == 1) ? ((sum - 1) >> 1) : (sum >> 1);
st.AWindow.PushBit(j, aJ);
anyOk = true;
break;
}
if (!anyOk)
{
if (DebugEnabled && st.Block <= DebugMaxBlocks)
{
BigInteger qPrefix = st.QBits.ToBigInteger();
if (qPrefix == DebugTargetQ)
{
Console.WriteLine(
$"[DEBUG CUT-BIT] block={st.Block}, col={j}, sumOther={sumOther}, carryIn={st.Carry}, hasPlanned={hasPlanned}, plannedAJ={plannedAJ}"
);
}
}
return false;
}
st.Carry = newCarry;
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 always
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ApplyANibble(ref State st, int startBit, byte aNibble)
{
// Apply 4 bits a_{startBit..startBit+3} into the rolling window.
// The window is addressed by absolute j; we push bits in order as columns advance, not at nibble application time.
// Here we store "scheduled" bits into a sparse map (only within window), so StepSingleColumn can read them.
for (int b = 0; b < 4; b++)
{
int bitPos = startBit + b;
int bit = (aNibble >> b) & 1;
st.AWindow.SetPlanned(bitPos, bit);
}
}
private readonly struct AllowedMaskKey(
int pBits,
int maxQBits,
int block,
int carry,
byte qNibble,
ulong qSig,
ulong aSig,
ushort candidateMask) : 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 readonly ushort CandidateMask = candidateMask;
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
&& CandidateMask == other.CandidateMask;
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, CandidateMask);
}
// Reduced state key for conservative per-block pruning.
// This key must be strong enough to avoid conflating states that can yield different allowed masks.
private readonly struct ReducedStateKey(
int pBits,
int maxQBits,
int block,
int carry,
ulong qPrefixSig,
ulong aSig) : IEquatable<ReducedStateKey>
{
public readonly int PBits = pBits;
public readonly int MaxQBits = maxQBits;
public readonly int Block = block;
public readonly int Carry = carry;
public readonly ulong QPrefixSig = qPrefixSig;
public readonly ulong ASig = aSig;
public bool Equals(ReducedStateKey other)
=> PBits == other.PBits
&& MaxQBits == other.MaxQBits
&& Block == other.Block
&& Carry == other.Carry
&& QPrefixSig == other.QPrefixSig
&& ASig == other.ASig;
public override bool Equals(object? obj) => obj is ReducedStateKey k && Equals(k);
public override int GetHashCode() => HashCode.Combine(PBits, MaxQBits, Block, Carry, QPrefixSig, ASig);
}
// Conservative per-block pruning statistics:
// For a given block, if a q-nibble is rejected (allowed mask == 0) for every reachable reduced state at that block,
// we can safely skip it for the remainder of the run.
private sealed class BlockPruneStats
{
private readonly Dictionary<ReducedStateKey, ushort> _stateRejectedMask = new(capacity: 1024);
public int SeenStates { get; private set; }
public ushort GlobalForbiddenMask { get; private set; }
// For each qNibble d, count how many reduced states have already rejected it.
private readonly int[] _rejectCounts = new int[16];
public void RegisterState(in ReducedStateKey key)
{
if (_stateRejectedMask.ContainsKey(key)) return;
_stateRejectedMask[key] = 0;
SeenStates++;
// New state potentially invalidates previously "global" forbids; recompute conservatively.
RecomputeGlobalMask();
}
public void MarkRejected(in ReducedStateKey key, byte qNibble)
{
if (!_stateRejectedMask.TryGetValue(key, out ushort mask))
{
// Should not happen if RegisterState() is called, but keep it safe.
_stateRejectedMask[key] = 0;
SeenStates++;
mask = 0;
}
ushort bit = (ushort)(1 << qNibble);
if ((mask & bit) != 0) return; // already counted for this reduced state
mask |= bit;
_stateRejectedMask[key] = mask;
_rejectCounts[qNibble]++;
// If all seen states reject this qNibble, we can forbid it globally at this block.
if (_rejectCounts[qNibble] == SeenStates)
GlobalForbiddenMask |= bit;
}
private void RecomputeGlobalMask()
{
ushort m = 0;
for (int d = 0; d < 16; d++)
if (_rejectCounts[d] == SeenStates && SeenStates > 0)
m |= (ushort)(1 << d);
GlobalForbiddenMask = m;
}
}
private struct State
{
public ABitsWindow AWindow; // rolling a window + planned bits map
public int BitIndex;
public int Block;
public int Carry;
public byte KFlushRemaining;
public bool KTerminated;
public byte PostTermZeroQNibbles;
public DynamicBitset QBits; // q bits [0..maxQBits-1]
public QGenState16 QState;
public static State CreateInitial(QGenState16 initQ, int maxQBits)
{
var s = new State
{
Block = initQ.NibbleIndex,
BitIndex = 0,
Carry = 0,
QState = initQ,
QBits = new DynamicBitset(maxQBits),
AWindow = new ABitsWindow(maxQBits),
KFlushRemaining = 0,
KTerminated = false,
PostTermZeroQNibbles = 0,
};
s.QBits.Set(0);
// a0=1
s.AWindow.SetPlanned(0, 1);
return s;
}
// Clone bitsets/windows to isolate branches.
// For performance, replace with copy-on-write/pooling.
public State CloneShallow() => new()
{
Block = Block,
BitIndex = BitIndex,
Carry = Carry,
QState = QState,
QBits = QBits.CloneShared(),
AWindow = AWindow.Clone(),
KFlushRemaining = KFlushRemaining,
KTerminated = KTerminated,
PostTermZeroQNibbles = PostTermZeroQNibbles,
};
}
// ----------------------------- Bit primitives -----------------------------
/// <summary>Dynamic bitset with fast enumeration of set bits up to a limit.</summary>
public sealed class DynamicBitset
{
private sealed class BitsetData
{
public readonly int Bits;
public ulong[] Words;
// Indices of words that are non-zero (monotone increasing in this scanner).
public int[] NzWords;
public int NzCount;
public BitsetData(int bits)
{
Bits = bits;
Words = new ulong[(bits + 63) >> 6];
NzWords = new int[16];
NzCount = 0;
}
public BitsetData(int bits, ulong[] words, int[] nzWords, int nzCount)
{
Bits = bits;
Words = words;
NzWords = nzWords;
NzCount = nzCount;
}
}
private readonly BitsetData _data;
private bool _shared; // copy-on-write guard
public DynamicBitset(int bits)
{
_data = new BitsetData(bits);
_shared = false;
}
private DynamicBitset(BitsetData data, bool shared)
{
_data = data;
_shared = shared;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureWritable()
{
if (!_shared) return;
// Deep copy underlying arrays.
var w = new ulong[_data.Words.Length];
Array.Copy(_data.Words, w, w.Length);
var nz = new int[_data.NzWords.Length];
Array.Copy(_data.NzWords, nz, nz.Length);
var copied = new BitsetData(_data.Bits, w, nz, _data.NzCount);
// Switch this instance to its private copy.
// Note: _data is readonly, so we rely on the fact that CloneShared creates a *new* DynamicBitset
// instance for children. This instance becomes non-shared by clearing _shared; its _data remains unique
// because no other instance references 'copied'.
//
// To preserve that with readonly _data, we keep _data as reference to mutable arrays inside BitsetData.
// Here we mutate the arrays in-place by replacing their references.
_data.Words = copied.Words;
_data.NzWords = copied.NzWords;
_data.NzCount = copied.NzCount;
_shared = false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddNzWord(int wIndex)
{
int n = _data.NzCount;
if ((uint)n >= (uint)_data.NzWords.Length)
{
// grow
int newLen = _data.NzWords.Length * 2;
if (newLen < 16) newLen = 16;
var tmp = new int[newLen];
Array.Copy(_data.NzWords, tmp, _data.NzWords.Length);
_data.NzWords = tmp;
}
_data.NzWords[n] = wIndex;
_data.NzCount = n + 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(int bit)
{
int bits = _data.Bits;
if ((uint)bit >= (uint)bits) return;
int w = bit >> 6;
int b = bit & 63;
ulong mask = 1UL << b;
ulong before = _data.Words[w];
if ((before & mask) != 0) return; // already set; no mutation needed
EnsureWritable();
before = _data.Words[w];
ulong after = before | mask;
_data.Words[w] = after;
if (before == 0 && after != 0)
AddNzWord(w);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Get(int bit)
{
int bits = _data.Bits;
if ((uint)bit >= (uint)bits) return 0;
ulong v = _data.Words[bit >> 6];
return (int)((v >> (bit & 63)) & 1UL);
}
/// <summary>Deep clone (copies backing arrays).</summary>
public DynamicBitset Clone()
{
var w = new ulong[_data.Words.Length];
Array.Copy(_data.Words, w, w.Length);
var nz = new int[_data.NzWords.Length];
Array.Copy(_data.NzWords, nz, nz.Length);
return new DynamicBitset(new BitsetData(_data.Bits, w, nz, _data.NzCount), shared: false);
}
/// <summary>
/// Shared clone (copy-on-write). Children share the same arrays until one of them mutates.
/// </summary>
public DynamicBitset CloneShared()
{
// Mark both this and the clone as shared so either mutation triggers EnsureWritable.
_shared = true;
return new DynamicBitset(_data, shared: true);
}
public ulong Signature64UpTo(int maxBitInclusive)
{
if (maxBitInclusive < 0) return 0UL;
int bits = _data.Bits;
int maxBit = Math.Min(maxBitInclusive, bits - 1);
int maxWord = maxBit >> 6;
int lastBitInWord = maxBit & 63;
const ulong FnvOffset = 1469598103934665603UL;
const ulong FnvPrime = 1099511628211UL;
ulong h = FnvOffset;
for (int i = 0; i <= maxWord; i++)
{
ulong v = _data.Words[i];
if (i == maxWord)
{
ulong mask = lastBitInWord == 63 ? ulong.MaxValue : ((1UL << (lastBitInWord + 1)) - 1UL);
v &= mask;
}
h ^= v;
h *= FnvPrime;
}
return h;
}
public ulong Signature64WithNibble(int startBit, byte qNibble, int maxQBits)
=> Signature64WithNibbleUpTo(_data.Bits - 1, startBit, qNibble, maxQBits);
public ulong Signature64WithNibbleUpTo(int maxBitInclusive, int startBit, byte qNibble, int maxQBits)
{
const ulong FnvOffset = 1469598103934665603UL;
const ulong FnvPrime = 1099511628211UL;
int bits = _data.Bits;
int maxBit = Math.Min(Math.Max(maxBitInclusive, 0), bits - 1);
int maxWord = maxBit >> 6;
int w0 = startBit >> 6;
int w1 = (startBit + 3) >> 6;
ulong add0 = 0;
ulong add1 = 0;
for (int b = 0; b < 4; b++)
{
int bitPos = startBit + b;
if (bitPos >= maxQBits || bitPos > maxBit) break;
if (((qNibble >> b) & 1) == 0) continue;
int wi = bitPos >> 6;
int bi = bitPos & 63;
if (wi == w0) add0 |= 1UL << bi;
else add1 |= 1UL << bi;
}
// enforce q0=1 always
if (w0 == 0) add0 |= 1UL;
else if (w1 == 0) add1 |= 1UL;
ulong h = FnvOffset;
for (int i = 0; i <= maxWord; i++)
{
ulong v = _data.Words[i];
if (i == w0) v |= add0;
if (i == w1 && w1 != w0) v |= add1;
if (i == maxWord)
{
int lastBit = maxBit & 63;
ulong mask = lastBit == 63 ? ulong.MaxValue : ((1UL << (lastBit + 1)) - 1UL);
v &= mask;
}
h ^= v;
h *= FnvPrime;
}
return h;
}
public BigInteger ToBigInteger()
{
// BigInteger expects little-endian byte array.
byte[] bytes = new byte[_data.Words.Length * 8 + 1];
int idx = 0;
for (int i = 0; i < _data.Words.Length; i++)
{
ulong v = _data.Words[i];
for (int b = 0; b < 8; b++)
{
bytes[idx++] = (byte)(v & 0xFF);
v >>= 8;
}
}
return new BigInteger(bytes);
}
/// <summary>
/// Allocation-free enumeration of set bits up to a limit (inclusive), iterating only over non-zero words.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SetBitsUpToEnumerable EnumerateSetBitsUpTo(int maxBitInclusive)
=> new(_data.Words, _data.NzWords, _data.NzCount, _data.Bits, maxBitInclusive);
public readonly struct SetBitsUpToEnumerable
{
private readonly ulong[] _words;
private readonly int[] _nzWords;
private readonly int _nzCount;
private readonly int _bits;
private readonly int _maxBitInclusive;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SetBitsUpToEnumerable(ulong[] words, int[] nzWords, int nzCount, int bits, int maxBitInclusive)
{
_words = words;
_nzWords = nzWords;
_nzCount = nzCount;
_bits = bits;
_maxBitInclusive = maxBitInclusive;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator() => new(_words, _nzWords, _nzCount, _bits, _maxBitInclusive);
public struct Enumerator
{
private readonly ulong[] _words;
private readonly int[] _nzWords;
private readonly int _nzCount;
private readonly int _maxWord;
private readonly int _lastBitInWord;
private int _i; // index in nzWords list
private int _w; // current word index
private ulong _word;
private bool _started;
public int Current { get; private set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator(ulong[] words, int[] nzWords, int nzCount, int bits, int maxBitInclusive)
{
_words = words;
_nzWords = nzWords;
_nzCount = nzCount;
if (maxBitInclusive < 0 || bits <= 0)
{
_maxWord = -1;
_lastBitInWord = 0;
}
else
{
int maxBit = Math.Min(maxBitInclusive, bits - 1);
_maxWord = maxBit >> 6;
_lastBitInWord = maxBit & 63;
}
_i = 0;
_w = 0;
_word = 0;
_started = false;
Current = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_maxWord < 0) return false;
if (!_started)
{
_started = true;
_i = 0;
if (!AdvanceToNextNonZeroWord())
return false;
}
while (true)
{
while (_word != 0)
{
int tz = BitOperations.TrailingZeroCount(_word);
Current = (_w << 6) + tz;
_word &= _word - 1;
return true;
}
_i++;
if (!AdvanceToNextNonZeroWord())
return false;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool AdvanceToNextNonZeroWord()
{
while (_i < _nzCount)
{
int w = _nzWords[_i];
if (w > _maxWord) return false;
_w = w;
_word = LoadWord(_w);
if (_word != 0) return true;
_i++;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ulong LoadWord(int w)
{
ulong word = _words[w];
if (w == _maxWord)
{
ulong mask = _lastBitInWord == 63 ? ulong.MaxValue : ((1UL << (_lastBitInWord + 1)) - 1UL);
word &= mask;
}
return word;
}
}
}
public void ForEachSetBitUpTo(int maxBitInclusive, Action<int> action)
{
if (maxBitInclusive < 0) return;
int bits = _data.Bits;
int maxBit = Math.Min(maxBitInclusive, bits - 1);
int maxWord = maxBit >> 6;
int lastBitInWord = maxBit & 63;
int nzCount = _data.NzCount;
int[] nzWords = _data.NzWords;
ulong[] words = _data.Words;
for (int i = 0; i < nzCount; i++)
{
int w = nzWords[i];
if (w > maxWord) break;
ulong word = words[w];
if (w == maxWord)
{
ulong mask = lastBitInWord == 63 ? ulong.MaxValue : ((1UL << (lastBitInWord + 1)) - 1UL);
word &= mask;
}
while (word != 0)
{
int tz = BitOperations.TrailingZeroCount(word);
int bit = (w << 6) + tz;
action(bit);
word &= word - 1;
}
}
}
}
/// <summary>
/// Rolling window of a-bits of length maxQBits with support for "planned" bits.
/// We separate:
/// - planned bits: explicit assignments for specific a_i within the window
/// - pushed bits: bits that have been committed as BitIndex advances
///
/// This allows nibble-level assignment while still streaming column-by-column.
/// </summary>
public sealed class ABitsWindow
{
private readonly int _windowBits;
private readonly int _wordCount;
// Committed bits are stored in a circular (bit-level) ring buffer.
// headBit points to the position holding the most recently committed bit (distance 0).
private readonly ulong[] _ring;
private int _headBit;
private int _currentJ; // last committed index j (monotone increasing)
// Planned a_i values (sparse) for not-yet-committed indices.
private readonly Dictionary<int, int> _planned;
public int CurrentJ => _currentJ;
public ABitsWindow(int windowBits)
{
_windowBits = windowBits;
_wordCount = (windowBits + 63) >> 6;
_ring = new ulong[_wordCount];
_headBit = -1; // "empty"
_currentJ = -1;
_planned = new Dictionary<int, int>(capacity: 1024);
}
private ABitsWindow(int windowBits, ulong[] ring, int headBit, int currentJ, Dictionary<int, int> planned)
{
_windowBits = windowBits;
_wordCount = (windowBits + 63) >> 6;
_ring = ring;
_headBit = headBit;
_currentJ = currentJ;
_planned = planned;
}
public ABitsWindow Clone()
{
var ring2 = new ulong[_ring.Length];
Array.Copy(_ring, ring2, ring2.Length);
var planned2 = new Dictionary<int, int>(_planned);
return new ABitsWindow(_windowBits, ring2, _headBit, _currentJ, planned2);
}
/// <summary>
/// A compact signature of the current a-window state (committed ring + planned bits).
/// Used to keep AllowedMaskCache semantically correct: allowed-a depends on previously committed a-bits.
/// </summary>
public ulong Signature64()
{
const ulong FnvOffset = 1469598103934665603UL;
const ulong FnvPrime = 1099511628211UL;
ulong h = FnvOffset;
h ^= (ulong)(uint)_windowBits; h *= FnvPrime;
h ^= (ulong)(uint)_headBit; h *= FnvPrime;
h ^= (ulong)(uint)_currentJ; h *= FnvPrime;
for (int i = 0; i < _ring.Length; i++)
{
h ^= _ring[i];
h *= FnvPrime;
}
// Planned bits: order-independent combine
ulong ph = 0;
foreach (var kv in _planned)
{
// mix index and bit (bit is 0/1)
ulong v = ((ulong)(uint)kv.Key << 1) ^ (uint)kv.Value;
ph ^= v + 0x9e3779b97f4a7c15UL + (ph << 6) + (ph >> 2);
}
h ^= ph;
h *= FnvPrime;
return h;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetPlanned(int aIndex, int bit)
{
if (bit != 0) _planned[aIndex] = 1;
else _planned.Remove(aIndex);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PushBit(int j, int bit)
{
// Commit bit for column j (a_j).
_planned.Remove(j);
_currentJ = j;
// Advance ring head and write the new bit.
_headBit = (_headBit + 1) % _windowBits;
int word = _headBit >> 6;
int b = _headBit & 63;
_ring[word] &= ~(1UL << b);
if (bit != 0) _ring[word] |= 1UL << b;
// planned map should not grow without bound; remove entries older than window.
int minKeep = j - _windowBits;
if (_planned.Count > 0)
{
var toRemove = new List<int>();
foreach (var kv in _planned)
if (kv.Key <= minKeep) toRemove.Add(kv.Key);
foreach (int k in toRemove) _planned.Remove(k);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetBitRelative(int aIndex)
{
// planned must have priority even if aIndex > _currentJ
if (_planned.TryGetValue(aIndex, out int plannedBit))
return plannedBit;
int d = _currentJ - aIndex;
if (d < 0 || d >= _windowBits) return 0;
if (_headBit < 0) return 0; // empty
int pos = _headBit - d;
pos %= _windowBits;
if (pos < 0) pos += _windowBits;
int word = pos >> 6;
int bit = pos & 63;
return (int)((_ring[word] >> bit) & 1UL);
}
/// <summary>
/// Try to read an a-bit if it is currently KNOWN (planned or already committed within the window).
/// Returns false when the bit is outside the committed window and not planned yet.
///
/// IMPORTANT: Callers that implement contradiction pruning must treat "unknown" as Unknown (may be 0 or 1),
/// never as 0, to avoid false negatives.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetKnownBit(int aIndex, out int bit)
{
// Planned has priority and is always a known assignment.
if (_planned.TryGetValue(aIndex, out int plannedBit))
{
bit = plannedBit;
return true;
}
// Not yet committed.
if (aIndex > _currentJ)
{
bit = 0;
return false;
}
int d = _currentJ - aIndex;
if (d < 0 || d >= _windowBits || _headBit < 0)
{
bit = 0;
return false;
}
int pos = _headBit - d;
pos %= _windowBits;
if (pos < 0) pos += _windowBits;
int word = pos >> 6;
int b = pos & 63;
bit = (int)((_ring[word] >> b) & 1UL);
return true;
}
public bool TryGetPlanned(int aIndex, out int bit)
=> _planned.TryGetValue(aIndex, out bit);
}
// ----------------------------------------------------------------------------
// q nibble generator for q = (2p)*k + 1 in base 16, without materializing k/q.
// ----------------------------------------------------------------------------
public readonly struct QGenState16
{
public readonly int NibbleIndex; // <-- DODAJ
public readonly uint Carry16;
public readonly ulong KWindowPacked;
public QGenState16(int nibbleIndex, uint carry16, ulong kWindowPacked)
{
NibbleIndex = nibbleIndex;
Carry16 = carry16;
KWindowPacked = kWindowPacked;
}
}
public sealed class QNibbleGenerator16
{
private readonly byte[] _mDigits; // nibbles of m=2p in base 16, LSB-first
private readonly int _mLen;
private readonly int _kWinLen; // mLen-1
private readonly ulong _kWinMask;
public QNibbleGenerator16(ulong p)
{
ulong m = checked(2UL * p);
_mDigits = ToBase16DigitsLSB(m);
_mLen = _mDigits.Length;
_kWinLen = Math.Max(0, _mLen - 1);
_kWinMask = _kWinLen == 0 ? 0UL : ((1UL << (4 * _kWinLen)) - 1UL);
}
public static QGenState16 InitialState => new(0, 0, 0);
public int KWinLen => _kWinLen;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ComputeNext(QGenState16 state, byte kNibble, out byte qNibble, out QGenState16 nextState)
{
ulong sum = state.Carry16;
int nibbleIndex = state.NibbleIndex;
// +1 only contributes to the very first nibble, once
if (nibbleIndex == 0)
sum += 1UL;
// j=0 term
sum += (ulong)_mDigits[0] * kNibble;
int maxJ = Math.Min(nibbleIndex, _mLen - 1);
for (int j = 1; j <= maxJ; j++)
{
byte mj = _mDigits[j];
byte kPrev = GetKFromWindow(state.KWindowPacked, j - 1, _kWinLen);
sum += (ulong)mj * kPrev;
}
qNibble = (byte)(sum & 0xFUL);
uint nextCarry = (uint)(sum >> 4);
ulong nextWin = state.KWindowPacked;
if (_kWinLen > 0)
nextWin = ((nextWin << 4) | kNibble) & _kWinMask;
nextState = new QGenState16(
nibbleIndex + 1,
nextCarry,
nextWin
);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ComputeAllNext(
QGenState16 state,
Span<byte> qNibByKn, // len=16
Span<QGenState16> nextByKn, // len=16
Span<int> countsByQn // len=16
)
{
ulong baseSum = state.Carry16;
int nibbleIndex = state.NibbleIndex;
// +1 only contributes to the very first nibble, once
if (nibbleIndex == 0)
baseSum += 1UL;
// Σ_{j=1..maxJ} mj * kPrev (part independent of current kn)
int maxJ = Math.Min(nibbleIndex, _mLen - 1);
for (int j = 1; j <= maxJ; j++)
{
byte mj = _mDigits[j];
byte kPrev = GetKFromWindow(state.KWindowPacked, j - 1, _kWinLen);
baseSum += (ulong)mj * kPrev;
}
ulong m0 = _mDigits[0];
for (byte kn = 0; kn <= 15; kn++)
{
ulong sum = baseSum + m0 * kn;
byte qn = (byte)(sum & 0xFUL);
uint nextCarry = (uint)(sum >> 4);
ulong nextWin = state.KWindowPacked;
if (_kWinLen > 0)
nextWin = ((nextWin << 4) | kn) & _kWinMask;
qNibByKn[kn] = qn;
nextByKn[kn] = new QGenState16(nibbleIndex + 1, nextCarry, nextWin);
countsByQn[qn]++;
}
}
private static byte[] ToBase16DigitsLSB(ulong x)
{
if (x == 0) return [0];
Span<byte> tmp = stackalloc byte[16];
int len = 0;
while (x != 0)
{
tmp[len++] = (byte)(x & 0xF);
x >>= 4;
}
var r = new byte[len];
for (int i = 0; i < len; i++) r[i] = tmp[i];
return r;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte GetKFromWindow(ulong packed, int idxFromNewest, int winLen)
{
// newest nibble is stored at LSB (shift 0)
// idxFromNewest=0 => most recently appended nibble
if ((uint)idxFromNewest >= (uint)winLen) return 0;
int shift = idxFromNewest * 4;
return (byte)((packed >> shift) & 0xF);
}
public static void SelfTest(ulong p)
{
var gen = new QNibbleGenerator16(p);
var st = InitialState;
// k = 4383 = 0x111F => nibble LSB-first: F,1,1,1,0,0,...
byte[] kNibs = { 0xF, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0 };
BigInteger q = BigInteger.Zero;
for (int i = 0; i < 12; i++)
{
byte kn = i < kNibs.Length ? kNibs[i] : (byte)0;
gen.ComputeNext(st, kn, out byte qn, out var ns);
q |= (new BigInteger(qn) << (4 * i));
st = ns;
}
Console.WriteLine($"[SELFTEST] p={p} q(hex)={q.ToString("X")}");
Console.WriteLine($"[SELFTEST] p={p} q(dec)={q}");
}
}
}