Benchmarks & Comparison
LogicalOptimizer is a dependency-free managed .NET Boolean toolkit. This page
summarizes how its result quality and speed compare to two widely-used two-level
minimizers — Python's SymPy (simplify_logic) and
PyEDA (Espresso) — on a shared corpus of Boolean
functions. The full methodology, the machine-readable BenchmarkDotNet suites and
the SAT-corpus perf-regression live in
doc/BENCHMARKS.md.
Head-to-head: result size (literals)
Both sides read the same committed corpus
(tools/comparison_corpus.txt).
Literal count — the number of variable occurrences in the minimized expression —
is machine-independent, so it is the real comparison. Bold = LogicalOptimizer
strictly smaller. Competitor numbers are from the Linux CI run
(sympy 1.14.0, pyeda 0.29.0).
| Function | Vars | LogicalOptimizer | SymPy | PyEDA |
|---|---|---|---|---|
| maj3 | 3 | 5 | 6 | 6 |
| consensus3 | 3 | 4 | 4 | 4 |
| xor2 | 2 | 4 | 4 | 4 |
| xor3 | 3 | 10 | 12 | 12 |
| maj4 | 4 | 8 | 12 | 12 |
| mux2 | 3 | 4 | 4 | 4 |
| pos6 | 6 | 6 | 24 | 24 |
| eq5 | 5 | 10 | 10 | 10 |
| pairs8 | 8 | 8 | 8 | 8 |
| pairs10 | 10 | 10 | timeout |
10 |
| pairs12 | 12 | 12 | timeout |
12 |
| collapse14 | 14 | 7 | timeout |
7 |
What the numbers say
Result size — never larger, often smaller. On every function LogicalOptimizer
matches or beats the two-level minimizers: maj3 5 vs 6, xor3 10 vs 12, maj4
8 vs 12, and pos6 6 vs 24. The reason is a genuine capability difference —
OptimizeExpression returns a multi-level (factored) form, while SymPy and
PyEDA return a two-level SOP. pos6 is the clearest case: a 6-literal
product-of-sums stays 6 literals for us but expands to a 24-literal DNF for a
two-level tool. That advantage is real, but it is not a like-for-like SOP proof —
for that, see the apples-to-apples two-level table below.
Head-to-head: two-level SOP result size (apples-to-apples, --dnf)
To compare like for like, run our two-level SOP path (compare --dnf) and count
result.DNF — the exact QM / SAT-cover / espresso-lite cover, the same two-level
form SymPy and PyEDA emit. OUR column is the real --dnf literal count; the
SymPy/PyEDA columns are the same Linux-CI numbers as the table above. Bold =
LogicalOptimizer strictly smaller.
| Function | Vars | LogicalOptimizer (--dnf) |
SymPy | PyEDA |
|---|---|---|---|---|
| maj3 | 3 | 6 | 6 | 6 |
| consensus3 | 3 | 4 | 4 | 4 |
| xor2 | 2 | 4 | 4 | 4 |
| xor3 | 3 | 12 | 12 | 12 |
| maj4 | 4 | 12 | 12 | 12 |
| mux2 | 3 | 4 | 4 | 4 |
| pos6 | 6 | 24 | 24 | 24 |
| eq5 | 5 | 10 | 10 | 10 |
| pairs8 | 8 | 8 | 8 | 8 |
| pairs10 | 10 | 10 | timeout |
10 |
| pairs12 | 12 | 12 | timeout |
12 |
| collapse14 | 14 | 7 | timeout |
7 |
On a genuine two-level basis LogicalOptimizer ties the specialized two-level minimizers cube for cube on every function where they finish (and matches PyEDA, which never times out, on all 12 rows). The multi-level column above is then an additional win on top — not the whole story.
Scale. SymPy builds a 2ⁿ truth table for Quine–McCluskey, so it degrades
sharply with variable count and times out (> 10 s) from 10 variables onward
(pairs10+). PyEDA (Espresso over the same truth table) and LogicalOptimizer both
stay in the low-millisecond range across the whole corpus. Beyond 14 variables
only LogicalOptimizer runs — both competitor tools build 2ⁿ rows and are
impractical there.
Note
Timings are wall-clock and machine-dependent; the head-to-head above compares
literal counts (machine-independent). For controlled timing, see the
BenchmarkDotNet suites in doc/BENCHMARKS.md.
Reproduce
# Our side (result size, minimality status, time per corpus function):
dotnet run -c Release --project LogicalOptimizer.Benchmarks -- compare
# Apples-to-apples two-level SOP (result.DNF), matching SymPy/PyEDA:
dotnet run -c Release --project LogicalOptimizer.Benchmarks -- compare --dnf
# Competitor side (SymPy + PyEDA; Linux/POSIX, where the per-function timeout fires):
python tools/compare_sympy_pyeda.py --max-vars 14 --timeout 10
The CI Comparison table step runs both on the Linux runner and prints them to the workflow log. The Python script self-skips any tool that is not importable and never fabricates numbers.
Regression corpora (generated, structured)
Beyond the 17-function comparison corpus, three generated, deterministic corpus
families back the regression suites — synthetic structured families, not industrial
workloads (the corpus-realism gap in doc/COMPETITIVE_ASSESSMENT.md is closed only in
part by them):
- SAT corpus (
LogicalOptimizer.Benchmarks/SatCorpus/) — 20 SATLIB-style DIMACS instances (planted 3-SAT, forced-UNSAT 3-SAT, pigeonhole); nightly correctness-under-budget regression. - Multi-output PLA corpus (
LogicalOptimizer.Benchmarks/PlaCorpus/) — 8 classic multi-output blocks (BCD-to-7-segment, adder, decoders, priority encoder, comparator, seeded random PLAs) in Espresso.plaformat; a gate-visible test optimizes all 35 outputs and pins per-output and per-file literal counts plus equivalence-guard andMinimalProvenstatuses. - Adversarial BDD order corpus (
LogicalOptimizer.Benchmarks/BddOrderCorpus/) — 8 order-sensitive formulas (bit comparators, disjoint-pairs DNF) whose ROBDD is linear in a good variable order and exponential in the adversarial one; a gate-visible test pins node counts under a small explicit budget, the documented typedNodeBudgetExceededExceptionon the adversarial order, and recovery via order heuristics / sifting.
All three regenerate byte-identically
(dotnet run -c Release --project LogicalOptimizer.Benchmarks -- generate-corpora for
the PLA/BDD families) and the committed files are verified against the generators in the
test gate. See doc/BENCHMARKS.md and each corpus README for the pinned tables.
Resource observability
Beyond elapsed time and allocated bytes, the full comparison suite
(-- comparison-suite) records the process-level peak working set per row and
per run (peakWorkingSetBytes — a monotone process high-water mark, so it is not
attributable to a single function; see the attribution caveat in
doc/COMPARISON_METHODOLOGY.md),
and a dedicated harness (-- cancellation-overshoot) measures cancellation
overshoot — the latency between a mid-flight cancel and each budgeted engine
actually returning control — as median/max over repeated runs. Both are
machine-dependent observability metrics: reported, never asserted.