Local ATP Benchmark
Mix.install([
{:atp_benchmark_runner, github: "penthooose/atp_benchmark_runner", force: true}
])
1. Configure paths from .env
# Load .env values into the OS environment (Config.get() and hpc_connect pick them up).
env_map = AtpBenchmarkRunner.load_env!(Path.expand("../.env", __DIR__))
IO.puts("Configured paths:")
IO.puts(" TPTP_ROOT: #{AtpBenchmarkRunner.tptp_dir()}")
IO.puts(" Store dir: #{AtpBenchmarkRunner.store_dir()}")
IO.puts(" SMT temp dir: #{AtpBenchmarkRunner.Config.smt_tmp_dir()}")
IO.puts(" THF temp dir: #{AtpBenchmarkRunner.Config.thf_tmp_dir()}")
This notebook is the local counterpart to benchmark_hpc.livemd. It runs the
same prover images (via Docker or Apptainer) and applies the same
input preparation as on the cluster:
:cvc5- problems are converted to SMT-LIB (TPTPToSMT) before cvc5 sees them (cvc5 has no TPTP input dialect); its baresat/unsat/unknownanswer is mapped to an SZS status.:lash- FOF/CNF/TFF problems are converted to THF (TPTP.ToTHF) because lash is a THF-$o-only prover.- All other provers read the TPTP file directly.
2. Detect available execution methods
AtpBenchmarkRunner.LocalRunner.print_image_status_summary()
Available provers:
:tableaux- uses the local escript (always available aftermix escript.buildin item #12):eprover,:vampire,:cvc5,:zipperposition,:leo3,:leo2,:lash- run via Docker images built from theirContainerfilesNative binaries of the reference provers are not distributed for Windows. The Docker images are built from the same sources as the HPC Apptainer
.defs, so results are directly comparable with the cluster.
3. Build / pull Docker images for all provers
This builds images for all registered provers from their Containerfiles
(Docker) or apptainer.defs (Apptainer). The first build downloads
dependencies and may take a few minutes; subsequent runs are instant.
force: true rebuilds everything. Returns {:ok, name} / {:error, name, reason}
per prover.
# Build (or pull) local images for all registered provers in one call.
# backend: :auto (default) | :docker | :apptainer · force: true rebuilds all.
results = AtpBenchmarkRunner.build_local_images!(:all, backend: :docker)
4. Download problems
Single problems are downloaded from the TPTP distribution into TPTP_ROOT
(C:\tmp\tptp from .env). download_tptp_problems!/2 returns the resolved
Problem structs plus any download warnings.
selected_problems = [
"ALG001+0.p",
"GRP001-0.p",
"GRP002+0.p",
"GRP003+0.p",
"GRP004+0.p",
"GRP720+1.p",
"GRP752-1.p",
"LAT001+0.p",
"PUZ001+0.p",
"REL001+0.p",
"SET001^0.p",
"SMT001+0.p",
"SYN000+0.p",
"THF001+0.p"
]
{downloaded, download_warnings} =
AtpBenchmarkRunner.download_tptp_problems!(selected_problems, force: false)
IO.puts("Downloaded #{length(downloaded)} problem(s):")
Enum.each(downloaded, fn problem ->
IO.puts(" ✅ #{problem.name} status=#{problem.expected_status} rating=#{problem.rating}")
IO.puts(" -> #{problem.path}")
end)
if download_warnings == [] do
IO.puts("No warnings.")
else
IO.puts("Warnings (#{length(download_warnings)}):")
Enum.each(download_warnings, fn w ->
IO.puts(" ⚠️ #{w.name}: #{inspect(w.reason)}")
end)
end
Tip:
download_tptp_problems!/2is the "single problem download" entry point. Pass any TPTP problem name (e.g."GRP001-0.p"); problems already present underTPTP_ROOTare reused (setforce: trueto re-download).
5. Select provers and problems
Problems are selected by name. They resolve from TPTP_ROOT, the
bundled examples, or a livebook cache automatically.
selected_provers = [
:tableaux,
:shot_tx,
:eprover,
:vampire,
:cvc5,
:zipperposition,
:leo3,
:leo2,
:lash
]
problems =
AtpBenchmarkRunner.select_problems(
names: selected_problems,
limit: 10
)
IO.puts("Provers (#{length(selected_provers)}): #{inspect(selected_provers)}")
IO.puts("")
IO.inspect(Enum.map(problems, &%{name: &1.name, path: &1.path}), label: "Problems")
6. Run the local benchmark
Each prover runs against each problem sequentially, exactly like the
single-node sequential HPC mode. auto_ensure_images: true builds any missing
Docker image on demand, and the same SMT/THF input preparation as on HPC is
applied per prover.
IO.puts("Starting local benchmark...")
IO.puts(" Provers: #{inspect(selected_provers)}")
IO.puts(" Problems: #{length(problems)}")
IO.puts("")
{t_ms, results} =
:timer.tc(fn ->
AtpBenchmarkRunner.local_benchmark(selected_provers, problems,
timeout_seconds: 30,
auto_ensure_images: true,
include_raw_output: true
)
end)
elapsed_s = t_ms / 1_000_000
IO.puts("Benchmark completed in #{Float.round(elapsed_s, 2)}s")
IO.puts("Total results: #{length(results)}")
7. Results table
# Guard: fall back to an empty list if this cell is evaluated before section 6.
results = binding()[:results] || []
AtpBenchmarkRunner.results_table(results)
8. Aggregated report
report = AtpBenchmarkRunner.report(results)
IO.puts("Run ID: #{report.run_id}")
IO.puts("Generated: #{report.generated_at}")
IO.puts("")
AtpBenchmarkRunner.print_report_markdown(report)
Per-prover breakdown
IO.puts("| Prover | Total | Solved | Failed | Solve Rate |")
IO.puts("|--------|------:|-------:|-------:|-----------:|")
Enum.each(report.by_prover, fn p ->
IO.puts("| #{p.prover} | #{p.total} | #{p.solved} | #{p.failed} | #{Float.round(p.solve_rate * 100, 1)}% |")
end)
Per-problem comparison
AtpBenchmarkRunner.print_per_problem(report)
Full explained results
IO.puts("Full explained results with proofs:\n")
AtpBenchmarkRunner.print_explain_full(results)
Filtered results by prover
target_prover = :shot_tx
AtpBenchmarkRunner.print_verbose_report(results, prover: target_prover, solved_only: false)
View a specific proof
target_prover = :shot_tx
target_problem = "GRP001-0"
AtpBenchmarkRunner.show_proof(results, target_prover, target_problem)
Raw prover debug output
AtpBenchmarkRunner.print_raw_output(results, :shot_tx, "GRP001-0")
Interesting findings
interesting = report.interesting
IO.puts("### Easy problems our prover failed (rating <= 0.3)")
if interesting.easy_failed_by_ours == [] do
IO.puts(" (none) 🎉")
else
Enum.each(interesting.easy_failed_by_ours, &IO.puts(" - #{&1}"))
end
IO.puts("")
IO.puts("### Hard problems our prover solved (rating >= 0.7)")
if interesting.hard_solved_by_ours == [] do
IO.puts(" (none)")
else
Enum.each(interesting.hard_solved_by_ours, &IO.puts(" - #{&1}"))
end
IO.puts("")
IO.puts("### Solved only by our prover")
if interesting.only_ours == [] do
IO.puts(" (none)")
else
Enum.each(interesting.only_ours, &IO.puts(" - #{&1}"))
end
IO.puts("")
IO.puts("### Solved only by reference provers")
if interesting.only_others == [] do
IO.puts(" (none)")
else
Enum.each(interesting.only_others, &IO.puts(" - #{&1}"))
end
9. Visualizations
Mermaid-based visual companions to explain/1 / explain_full/1. Each cell
returns a Kino.Mermaid diagram (or a fenced markdown block outside
Livebook).
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
target =
Enum.find(results, fn r ->
r.prover == :eprover and AtpBenchmarkRunner.Result.solved?(r)
end)
if target, do: AtpBenchmarkRunner.visualize(target), else: "No solved eprover result found."
9b. Proof dependency graph
For provers that emit machine-readable TPTP clause refutations (E, Vampire) this renders the proof as a dependency DAG. Other provers fall back to a pipeline diagram.
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
target =
Enum.find(results, fn r ->
match?({:ok, _}, AtpBenchmarkRunner.Visualize.Proof.parse(r))
end)
if target, do: AtpBenchmarkRunner.visualize_proof(target), else: "No parseable proof found."
9c. Run summary (status + wall-time chart)
AtpBenchmarkRunner.visualize(results) is the one-call shortcut for
Visualize.status_pie/2 + Visualize.timeline/2; it pushes both diagrams as
Livebook outputs.
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
case AtpBenchmarkRunner.visualize(results) do
:ok -> :ok
markdowns -> Enum.each(markdowns, &IO.puts/1)
end
9d. Report scoreboard
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
report = AtpBenchmarkRunner.report(results)
AtpBenchmarkRunner.visualize(report)
9e. Raw Mermaid source (copy into a markdown cell)
results = binding()[:results] || []
results = if results == [], do: VisualizeSample.results(), else: results
target =
Enum.find(results, fn r ->
match?({:ok, _}, AtpBenchmarkRunner.Visualize.Proof.parse(r))
end)
if target do
IO.puts(AtpBenchmarkRunner.Visualize.markdown(AtpBenchmarkRunner.Visualize.proof(target)))
else
IO.puts("No parseable proof; nothing to show.")
end
10. Persist results to local store
run = AtpBenchmarkRunner.new_run(
title: "Local smoke test",
problems: problems,
provers: selected_provers,
walltime: "00:30:00",
problem_timeout_seconds: 30
)
paths = AtpBenchmarkRunner.persist_run!(run, results, report)
IO.puts("Run saved: #{paths.run_path}")
IO.puts("Results saved: #{paths.results_path}")
IO.puts("Report saved: #{paths.report_path}")
Runs are persisted to the store directory from .env
(ATP_BENCHMARK_RUNNER_STORE_DIR, e.g. C:\tmp\atp_benchmark_runner_store).
11. Compare with a previous run (longitudinal diff)
prev_results_path = nil
if prev_results_path && File.exists?(prev_results_path) do
prev_results = AtpBenchmarkRunner.Store.load_results!(prev_results_path)
diff = AtpBenchmarkRunner.compare_runs(prev_results, results)
AtpBenchmarkRunner.print_diff(diff)
else
IO.puts("No previous results to compare against. Set `prev_results_path` to enable.")
end
12. Cross-run comparison & statistics
Compares all stored runs at once. Results stay in JSON files under the store dir.
Ranking counts Timeout/GaveUp as real attempts (never excluded). By default
InputError/TypeError/UnsupportedLogic/Error are treated as non-attempts
(prover not applicable to that problem); override with :exclude_statuses
(pass [] to include them).
# One call: load all runs, aggregate, and print the comparison tables.
stats = AtpBenchmarkRunner.compare_all_runs()
# Livebook-friendly panel (Kino.Markdown when available).
AtpBenchmarkRunner.MultiRun.panel(stats)
# Save the comparison as a Markdown file.
path =
AtpBenchmarkRunner.save_multi_run_stats!(
Path.join(AtpBenchmarkRunner.store_dir(), "comparison.md")
)
IO.puts("Comparison saved: #{path}")
# Same-problem-set ranking + error-status control.
# only_common_problems: rank only on problems every prover ran.
# exclude_statuses: [] keeps input/type errors as real attempts.
stats_common =
AtpBenchmarkRunner.multi_run_stats(nil,
only_common_problems: true,
exclude_statuses: []
)
AtpBenchmarkRunner.MultiRun.print(stats_common)
13. Inspect raw output (optional)
target_index = 0
if results != [] do
r = Enum.at(results, target_index)
if r && r.raw_output do
IO.puts("Raw output for #{r.problem_id} / #{r.prover}:")
IO.puts("")
IO.puts(r.raw_output)
else
IO.puts("No raw output stored (rerun with include_raw_output: true)")
end
else
IO.puts("No results available.")
end