Reproducible experiments#
An experiment needs a question, a fixed input set, exact configurations, and a decision criterion. The correctness gate applies before performance conclusions. Use statistics to check that the mechanism under investigation actually ran.
Prepare the comparison#
Record candidate and baseline commits, dirty-tree status, build type, compiler,
CaDiCaL signature, options, input manifest, expected answers, time/memory limits,
and output directory. Check each executable’s --version; a workspace’s Git
revision does not establish which binary was executed. Preserve an exact baseline
binary or build it from an isolated checkout.
The supplied SLURM workflows are reproducible execution options; contributors
can use another adequate test environment while recording the same provenance.
For the maintainer’s cluster, use
workspace preparation
with --build-via-slurm before submitting jobs, as required by the
workspace execution policy.
Read its --help for connection and staging options.
It synchronizes tracked files; ignored seed corpora must be staged after each
sync. Check dependencies and corpus paths on the execution host, not only on
the submission host. Do not substitute a symlink for an in-workspace corpus
when the harness resolves paths relative to that workspace.
Use one output directory per run. Confirm both baseline and candidate execute correctly through the actual harness before launching the full set. Record array size and expected rows per shard; a missing shard is not a timeout row.
Select inputs and interpret outcomes#
analyze_smt2.py reports declaration counts and Real-arithmetic shape/polarity summaries:
python3 scripts/analyze_smt2.py --json path/to/input.smt2
The analyzer expands lexical let bindings, including nested lets and
shadowing. Quoted and simple spellings of the same name share one binding
(x and |x|); duplicate equivalent bindings are rejected. Each parallel right-hand side is expanded in the outer environment;
only the body sees the new bindings. For example,
(let ((x 1) (y x)) (+ x y)) expands to (+ 1 x), preserving the free x.
Malformed or duplicate bindings and unsupported binders (forall, exists,
lambda, match, and function definitions) produce errors. This tool does
not reproduce frontend/session semantics; consult the
frontend for the solver’s binding rules.
The expansion follows the parallel-substitution semantics of SMT-LIB 2.7,
section 3.6.1, Let, and symbol identity in section 3.1 [4]. Its inline_lets function owns an explicit
stack of visit/body frames, dictionaries of expanded bindings and fresh list
trees. Right-hand-side frames finish before the body installs bindings;
substituted trees are copied without revisiting their free names. These are the
scope and capture-avoidance invariants checked by the parallel-binding,
shadowing and deep-nesting regressions. Expansion is iterative but can use
exponential time and space when a term is repeatedly copied; direct DAG
traversal remains deferred.
Text search alone cannot establish which theory or transformation a file uses.
Keep corpus exclusions explicit: parse rejection, unsupported constructs,
missing files and reference disagreements cannot silently disappear from the
denominator. An SMT-LIB :status is an expected answer, not an independent
proof. Inspect both answer text and process exit status, including multi-query
response counts and abnormal exits after an answer was printed.
Run speed measurements without independent validation in the timed interval.
Then validate SAT models separately, using the same binary,
options and inputs. Stop tuning on a wrong answer, invalid model, crash or
untriaged disagreement. Report timeouts and unknown separately from passes.
Tactic-composition checks#
The composition manifest
defines legal profiles, their logic sets, and required mechanism counters.
The driver compares references,
checks answers, and invokes independent model validation. It selects successful
SAT measurements and deduplicates them by profile and resolved input path.
The model stage uses --expect-sat: a replay changing SAT to UNSAT fails.
python3 scripts/tactic_composition_matrix.py \
--manifest benchmarks/tactic-composition-matrix.json --root . \
--solver ./build/primo --refs contrib/local/bin/z3 contrib/local/bin/cvc5 \
--dolmen contrib/local/bin/dolmen --timeout 10 --dolmen-timeout 20 \
--repeat 1 --out results/composition-review
Each profile’s model-validation/coverage.json joins validation outcomes back
to every selected measurement, including repeats, and records eligible,
validated and incomplete model counts. Missing, duplicate or unexpected
validator rows fail the check, even when the validator exits successfully.
An empty selection reports zero eligible models and skips the validator.
--allow-inconclusive permits timeout/unknown replays only as incomplete
validation; it never permits changed answers. Skipping validation does not
complete the qualification gate. Historical results retain their original
selection and must not be relabeled as evidence from this repaired driver.
The SLURM matrix
adds generated cases by logic. Do not require every tactic on every input:
composition can stop after a terminal result. A per-input requirement checks
that the intended route was reached on that case; an aggregate requirement
checks that a tactic was exercised somewhere in the eligible corpus. Preserve
both checks. Report inconclusive rows accepted by --allow-inconclusive
separately; that switch does not turn them into successful solves.
Summarize before deciding#
python3 scripts/summarize_benchmark_shards.py --timeout 30 \
--run baseline=results/baseline --run candidate=results/candidate \
--baseline baseline --candidate candidate
Inspect expected/completed rows, duplicate inputs, solved counts, PAR2, recoveries, losses, decisive-answer disagreements, family results, and common solved timings. PAR2 charges an unsolved instance twice the specified timeout; it must use the same input set and limit on both sides. For native sessions, whole-session completion, partial query progress and per-query time are different measurements. Keep them separate.
An aggregate loss can still reveal a useful portfolio configuration if it solves inputs the others miss. Record that complementarity, but do not promote the configuration to default on that basis alone. Record rejected experiments as rejected; remove their source changes before closing the investigation.
Evidence storage#
The authoritative ledger remains the run log bundled with this manual. Follow the recording contract: stable run ID, timestamps, kind/scope, exact commit and commands, generator/seed/count, references, environment/job IDs, limits, artifact paths, summary counts, evidence status, and limitations. Commit metadata and reproducibility scripts with the change relying on them. Do not add a solver run entry for a prose-only documentation build.
Git-based run directories and a derived SQLite query index are an agreed future storage design. They are not current services. Large raw artifacts may remain outside Git, but the ledger must make them identifiable and retrievable. A record marked current describes its own measured revision and scope, not every subsequent commit.
Refreshing a timing baseline#
Use a new experiment workspace and result directory for a complete refresh.
Build the selected clean revision with
the preparation helper
and its --build-via-slurm option, finish its
correctness gates, and preserve the candidate and reference binaries. A solver
change requires fresh Primo measurements. Reuse historical reference timings
when their input hashes, binaries, options, time limits and machine/resource
conditions support the intended comparison. Identify their origin explicitly;
do not label reused rows as fresh. Rerun reference timings when requested or
when the existing evidence cannot support the comparison. Run independent
model replay after all timing jobs finish.
The recipes under scripts/qualification/evaluation_refresh/ support the corpus
checks for this workflow. The seed recipe
checks deterministic ground seeds before timing. The
QF_LIA staging recipe
verifies the published checksum of the official
SMT-LIB 2025 archive before extracting it into a fresh directory. The caller
supplies the archive and maps corpora/LOGIC to each selected input root.
The inventory recipe
records every input’s relative path, size and SHA-256, the Primo, Z3, cvc5 and
Dolmen identities, and the allocated machine’s CPU information. These are
experiment artifacts, not timing samples.
Use the corpus job recipe
and benchmark/replay driver
for separate benchmark and replay phases. They use
ROOT/LOGIC/{primo,z3,cvc5}/shard-N result directories. The
refresh summarizer takes --root, --inventory, --timeout
and --out; it requires complete timing shards, exact inventory coverage,
matching binary hashes and the intended profiles (--quiet for Primo, no extra
reference arguments). To reuse audited references, pass --reference-map FILE,
where the JSON file maps each logic to z3 and/or cvc5, each containing root
(the directory containing historical shard-* directories) and measurement_note (the resource and
hardware comparison and its limitations). The same identity checks apply.
Omitted references are reported as unavailable; partial fresh rows do not fill
them. Hardware/resource comparability must be reviewed separately: the script
records the note but does not verify it. Without this option, all three solver
directories are required and labelled fresh.
The report includes family counts, PAR2, solved-runtime median and
nearest-rank 95th percentile, paired Primo/reference ratios and all decisive
answer disagreements. Independent model coverage remains a separate field;
complete timings alone do not qualify models. Timeout and unknown answers are
unsolved and receive the PAR2 penalty. Native session measurements use their
own query-aware drivers and whole-session limits rather than this single-query
adapter. The native-session summarizer
accepts repeated --family options for archive path components; use
--family hybrid_networks --expected-sessions 10 for the native LRA set.
Its default grouping remains the two native UF families. Its timing and
validation roots must be selected explicitly; it does not verify historical
reference provenance on the caller’s behalf.
For the six-division refresh with 1,778 native UF and ten native LRA sessions,
the final report job
joins the fixed results/refresh-* directories after replay. Set PRIMO_WORKDIR
and PRIMO_REPORTING_DIR to the experiment workspace and its preserved reporting
package. Supply the audited ground reference map and native UF reference-audit
JSON files first. Native LRA reference timing is marked unavailable in this
recipe. The job runs only metadata tools, writes coverage status, and returns
nonzero for incomplete Primo timings, answer failures, disagreements, or missing
supported model qualification. Quantified UF models remain outside independent
ground-model validation. A zero exit does not remove the stated limits of the
correctness samples or the historical timing comparisons.
A refreshed baseline does not erase earlier runs. Retain their revisions and limitations, and record a new ledger entry naming the fresh source revision, corpus identities, profiles, limits, jobs, raw artifacts and qualification state.