Skip to main content

Architecture

How pencilmark is put together: where each job lives, how a card is computed, and what must stay true. It names files and functions so you can find them; it does not link to lines, which move. The code is being restructured (registries for techniques, rule families and formats; a grader model object); this page describes the code as it is and is updated with each step.

What happens in pencilmark.rate(puzzle)​

  1. Read. api.load turns any accepted input (string, JSON, file, link) into a model.Puzzle. The public format is read by exchange/ (sudoku-puzzle, docs/formats/puzzle-format.md), text forms by interop.py, links by sources/.
  2. Check uniqueness. cpsat_verify.decide runs the C++ search (cbackend.brute_solve), with OR-Tools as an optional fallback; it raises NotUnique / NoSolution / Undecided.
  3. Find the human route. band_path.band_path solves band by band: at each position it asks the engine for every available move (cbackend.state_moves), names each one (band_path.classify) and takes the easiest. The result is the list of steps and the band.
  4. List the techniques. band_path.contents re-runs the route with techniques banned to find the shortest list and the interchangeable ones (the chips in contains).
  5. Score. card_score.score_parts prices every step and maps the average effort per cell to difficulty; export.time_units turns total effort and board size into time (data/time_model.json).
  6. Assemble. export.grade builds the card with only the requested fields, each from its builder in the card-field registry (export.FIELDS, in the card's key order); explain/ writes step explanations, publish_flags.py the publishing checks, se.py the Sudoku Explainer-style rating.

Layers​

LayerWhereOwns
Public APIapi.py, __main__.py (CLI)entry points; nothing else imports them
Formatsexchange/ (common, board, validate, read, write, canonical), rules/, puzzle_json.py, interop.py, spec_format.py, sources/reading and writing puzzles, specs and cards
Modelmodel.pythe Puzzle: cells, houses, relations, constraints; compiled by finalize()
Enginecsrc/ via cbackend.py (engine_records.py decodes its move records)every search: techniques, move listing, brute force, refutation
Graderband_path.py, card_score.py, scoring.py, metrics.py, export.py, card.py, grader_model.pyroute, prices, difficulty, time, band, contents; grader_model.py holds every fitted parameter, loaded once
Wordsexplain/ (base, grid, catalog, singles, combos, notes, chains, trial, records, dispatch), variants.py, usage.pyexplanations, variant names and rule text
Generatorgen/puzzles for a sudoku-spec request
Registryregistry.py over data/techniques.csv, codes.csv, patterns.csvevery fact about a technique, chip code and engine pattern code (name, band, order, family, flags, SE rating)
Datadata/*.json, data/*.csvfitted parameters and tables

The engine (csrc/)​

FileHolds
solver.hpp, solver.cpptechnique ids, move records, pricing, strategies and tiers, the solve loop, fallbacks (solver_util.hpp: shared helpers)
direct_view.cpp, notes_view.cpp, nary_moves.cppthe no-notes view; techniques on written candidates (singles, intersections, subsets, fish, wings, colouring); moves of cages, lines and sums
brute_force.cppexhaustive search (uniqueness, generator)
model.hpp, model.cpp, nary.hpp, nary.cpppuzzle structure in C++; multi-cell constraints (cages, lines) and their propagation
chains.cpp, colour.cpp, forcing.cppX-chains and AICs, colouring and Medusa, forcing chains
uniqueness.cpp, als.cpp, set_logic.cppuniqueness (UR / BUG) and implied houses; almost locked sets; set theory, digit counting, sum algebra, band logic (move_util.hpp: their shared helpers)
sudokuwiki.cppnamed patterns from Andrew Stuart's list (wings, Sue de Coq, fireworks, ...)
variant_logic.cpp, mental.cppvariant techniques; the no-notes layer (what a solver sees without pencil marks)
refute.cpp, desc.cpp, explain.cpp, canon.cpp, loads.cppsearch-effort profile, move descriptors, explanation records, symmetry helpers, cognitive load
listing_cache.cppthe listing caches (below)
bindings.cppthe pybind11 module pencilmark._csolver

A card lists the same positions many times: band by band at each step, again in every banned-technique route of the technique list, and again in the card's solves. listing_cache.cpp keeps, per thread, what each strategy found at an exact position. The disabled-technique flags a strategy reads are recorded as it reads them (Flags), so a stored result is reused only where those flags match. Single-digit techniques (fish, pattern overlay) also memoise per digit, on that digit's candidates. Both caches keep two generations within a fixed budget. _csolver.listing_cache(False) turns them off; tests/test_listing_cache.py checks that the move lists stay the same. band_path also lists the costliest late techniques (LATE) only when they could still give the move it picks (state_moves(skip=...)).

Invariants​

  • Deterministic. The same puzzle gives the same card on every machine and in every process. Anything ordered by a Python set or by time is a bug, and so is a result that depends on what a cache already holds. tools/golden.py check proves it for 55 puzzles.
  • Solver logic in C++. Python names, prices and explains moves; it does not search the grid.
  • Formats are strict. Unknown keys and reserved parts are refused with a message; format changes bump format_version.
  • Fitted numbers live in data/, never inline, and each says where it came from.

Known rough edges​

Being fixed by the restructure (docs/dev/code-style.md says how code should look):

  • Adding a technique still touches the C++ engine (enum in solver.hpp, the technique, its tier, its descriptors), the constant in solver.py, a row in data/techniques.csv (and codes.csv for a new chip), its fitted price, its explanation and, for extra move fields, cbackend.XMETA_KEYS. tests/test_registry.py fails until the registry and the engine enum agree.
  • A rule family is represented four times (public format, puzzle_json, model.Puzzle, the C++ spec).
  • explain/ takes each move's pattern from the engine's record, but most handlers still rebuild it to check it and to word it.
  • Parameters are keyed by internal technique ids; grader_model.py loads them once, but they are not yet addressed by name.

Changing things safely​

python -m pytest -q -n auto tests # about 10 seconds (30 s without -n)
python tools/golden.py check # every output of 55 puzzles must be identical (about a minute)

A change that is meant to alter output rewrites the golden records (tools/golden.py write) in its own commit, with the reason in the message.