feat: layered-stack solvers, exact columnar, hill-additive, telemetry - #8
Merged
Conversation
Three decompositions, each turning a joint search into a cheaper staged one. stack.py / stack_gpu.py — staged attack on stacks of transpositions under a periodic substitution. A transposition preserves monogram frequencies, so a periodic substitution sitting OUTSIDE any stack of transpositions peels off the RAW ciphertext by per-coset chi-square, with no transposition search at all. That fails when cosets are short (a period-45 key over 224 letters leaves 5 letters per coset), so the coupled search in layered.crack_layered is kept alongside it. Batching the inner DP across candidates on GPU — one kernel launch per mask for the whole batch — gives 124x: 99k-123k evals/s against 799 on CPU, verified bit-equal against the scalar solver. Measured depth curve on width-9 columnars over ~315 letters: depth 1 exact with no search, depth 2 exact in 2.3s on GPU, depth 3 does not converge. That last one is a SIGNAL wall, not a compute wall — 6.5M evaluations, best score flat after generation 10 for 390 more generations. At depth 2 a correct outer order immediately exposes English through the exact inner solve, so there is a gradient; at depth 3 both outer layers must be near-right before anything shows, so single-swap mutations get no partial credit. Unicity is not the obstacle (~55 bits against ~1000 bits of English redundancy at that length). Documented in the module so nobody spends a week throwing hardware at it. columnar_exact.py — the innermost columnar of a stack is exactly solvable. Its columns are PLAINTEXT columns, so the objective decomposes pairwise and Held-Karp gives the optimum in O(2^w * w^2) instead of O(w! * n). Width 9: 41k ops against 363k full decodes; widths 12-16 become reachable. Wide widths are declared out of reach and refused rather than hanging silently. hill_affine.py + ciphers/hill_additive.py — CT = M*(P + K) for a Hill matrix over a periodic additive. Applying any candidate inverse gives M^-1*CT = P + K, so the additive never has to be searched jointly with the matrix: what is left is an ordinary periodic Vigenere, solved by chi-square plus a quadgram polish. 26^9 collapses to a dictionary scan. telemetry.py — live progress and rate reporting for long sweeps. A no-hit sweep holds a steady rate for its full duration while a hit short-circuits early, so the rate itself is early evidence, visible from the first heartbeat rather than at the end. Two harness lessons encoded as guards: a bare `except: pass` around sub-solvers hid an ImportError for a whole regression run (a wrong answer won the ranking instead), and a startswith(16 chars) regression check passed a decode whose error started at char 17 — regression checks now compare full plaintext. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159mM39E2XA2ExfLsB1VUXW
- optional-dependency sentinels (numpy in columnar_exact/hill_affine, torch in stack_gpu) are annotated Any so the None fallback is not an assignment to a Module-typed name - Progress.predict narrows limit before formatting it - annotate the two inverse-index caches and the candidate list whose element type widens across branches - deep-stack reconstruction raises instead of indexing a possibly-None best order, which is also the honest behaviour when no generation scored No behavioural change; ruff and mypy both clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159mM39E2XA2ExfLsB1VUXW
…out the extra The previous attempt annotated the sentinels `Any`, which type-checks only when the module RESOLVES. mypy runs with ignore_missing_imports, so in an environment lacking the package the import itself binds the name and the None fallback becomes a redefinition — which is why CI failed on torch while local passed. - numpy (columnar_exact, hill_affine): match the precedent already used by scoring.py and analysis.py — a type: ignore[assignment] on the sentinel. numpy is installed in CI, so the ignore is always used and warn_unused_ignores is satisfied. - torch (stack_gpu): torch is an optional extra and is absent from the CI type environment, so neither form works in both — a bare sentinel is Module-vs-None where it resolves, and a type: ignore is unused where it does not. Type-check the import under TYPE_CHECKING and run the fallback at runtime, which is redefinition- free either way. Verified against two interpreters: the dev venv (torch + numpy present) and a torch-free venv that reproduces the CI type environment. Clean in both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159mM39E2XA2ExfLsB1VUXW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.