The tropical-matrix family: what the formulas actually say#
Structural analysis of the tropical-matrix QF_LRA family, written 2026-08-04
after reading the benchmark source rather than inferring shape from solver
counters. It exists because every prior attempt on this family
(QF_LRA Speedup Heuristics: Design Note‘s T1, the polarity follow-up, and the
row-propagation experiment) treated the instances as an opaque “hard family”
and tuned solver knobs against them. Reading the encoding changes what looks
promising, and rules out one idea that looked obviously right.
All measurements below are local (workstation, Release build/primo) and
are diagnostic, not benchmark claims. The A/B in Unguarding is sound, and 27x slower – the guards are load-bearing is the one
that matters and is recorded in docs/run-log.json.
The encoding is a tropical (arctic) semiring, not general arithmetic#
The benchmarks’ own :source header says they come from automated program
termination analysis, submitted by Johannes Waldmann in 2012, and that they
should be satisfiable. The body confirms a specific and very regular encoding.
Every quantity is a pair of SMT variables:
(declare-fun f12m () Bool) ; "this entry is -infinity"
(declare-fun f12c () Real) ; its finite value, meaningful only when f12m is false
On constraint-199552.smt2 the pairing is exact: 954 m/c pairs (36 f*
and 918 l*), 1,908 declarations, no ite, and – worth stating because a
careless grep suggests otherwise – zero multiplications. (* counted
with a basic regular expression matches “zero or more open parens”; the true
count of (* is 0. The logic really is linear.
f* variables are the free unknowns (the matrix coefficients being solved
for). l* variables are derived: each is one tropical operation applied to
two earlier quantities. Tropical multiplication is ordinary addition, and it
appears as a matched pair of definitions:
(= l51m (or f12m l3m)) ; -infinity is absorbing -- unconditional
(or l51m (= l51c (+ f12c l3c))) ; the value -- guarded
There are 468 of each on this instance. Comparisons take the dual form, where the guard is a negative conjunct instead of a positive disjunct:
(or l717m (and (not l870m) (>= l717c l870c)))
So the family is a DAG of definitions, not a constraint system: 918 derived
quantities over 36 genuinely free real unknowns, plus a Boolean layer deciding
which of them are -infinity.
The c variables are genuine don’t-cares under their own flag#
The structural property that makes the encoding work: whenever lKm is true,
lKc’s value is irrelevant everywhere in the formula, not just in its own
definition.
Verified rather than assumed, with a checker that inlines the let bindings
and walks the term with an explicit stack, treating both guard polarities –
a positive sibling disjunct lKm inside an or, and a negative sibling
conjunct (not lKm) inside an and:
quantity |
value |
|---|---|
distinct |
954 |
total occurrences |
5,106 |
occurrences protected by their own flag |
3,774 (73.9%) |
variables with every occurrence protected |
774 |
variables with some unprotected occurrence |
180 (36 |
guarded arithmetic definitions |
468 |
of those, fully protected |
468 (all) |
Handling only one guard polarity gets this badly wrong – it reports 0 of 468 protected – which is worth recording as a trap for anyone repeating the analysis.
So for all 468 definitions, (or lKm (= lKc e)) may soundly be replaced by the
unconditional (= lKc e): any model of the original with lKm true can be
repaired by setting lKc := e, since nothing else observes lKc in that case.
Unguarding is sound, and 27x slower – the guards are load-bearing#
That soundness argument suggests an obvious preprocessing win: drop the 468 guards, turning Boolean case splits into unconditional definitions that Gaussian elimination could substitute away. Measured, it is a large regression. Same binary, same workstation, load 2.1 both times:
version |
answer |
time |
decisions |
conflicts |
pivots |
pivot term ops |
|---|---|---|---|---|---|---|
original (guarded), |
sat |
4.13s |
15,766 |
93 |
7,679 |
128M |
468 definitions unguarded |
sat |
112s |
125,603 |
13,359 |
526,566 |
3,831M |
Both answer sat, which is the transformation’s soundness showing up
empirically. But it is 27x slower, with 68x the pivots.
The reason is the point of the whole encoding, and it inverts the intuition:
the guard is what lets the solver avoid the arithmetic. Setting lKm true
discharges the definition at the Boolean level for free. Unguarding forces all
468 equations permanently into the tableau, so every one of them must be
maintained whether or not that entry is -infinity. The formulas are already
well-encoded for a lazy SMT solver; the case splits are cheaper than the rows
they avoid.
This is the most useful negative result in this note. It rules out the entire family of “simplify the guards away” preprocessing ideas, T1’s distribution being the one variant that helps precisely because it keeps the guard.
Where the time actually goes: 42x fill-in#
Counters from the 4.13s solved run (constraint-199552, the only file Primo
solves at 60s):
counter |
value |
|---|---|
|
1,440 |
|
3,519 |
initial average row length |
2.44 |
|
164 rows touched per pivot |
|
~102 terms per row update |
implied fill-in |
~42x |
Rows start at 2.44 nonzeros and are ~102 terms long during search. That is the
same diagnosis QF_LRA Speedup Heuristics: Design Note section 6.2 reached globally, in an
extreme form – and here the cause is legible from the encoding rather than
inferred. Pivoting composes the definition DAG: substituting one definition
into another walks the chain back toward the 36 free f variables, so a
fully-composed row spans them all. The dense limit is structural, not
accidental.
Also worth noting: :lra.fixed-variables 0 and :lra.elimination-pivots 0 –
Primo’s existing elimination machinery does nothing at all on this family.
What this suggests, ranked#
None of these is implemented. They are recorded here rather than in QF_LRA Speedup Heuristics: Design Note‘s item list until one is actually tried, and Open leads register is the register that tracks their current status – L3 in particular has since been downgraded there.
L1 – Decide the m skeleton before the arithmetic (cheap). The Boolean
layer is self-contained: 468 unconditional definitions
(= lKm (or lIm lJm)) over 36 free f*m inputs, referring to no arithmetic
whatsoever. Which guards fire – hence which arithmetic is even relevant – is
fully determined by those 36 Booleans. Yet Primo’s decision order interleaves
m variables with theory atoms. The solved run shows 15,766 decisions against
only 93 conflicts: the search is nearly conflict-free, it is just walking,
and it is doing tableau work along the way that the right m assignment would
have made unnecessary. A branching-order preference for m variables over
arithmetic atoms is a small change with a clear rationale. Note this is
variable ordering, distinct from the phase selection already tried in the
polarity follow-up – and the phase result is corroborating evidence, since
--theory-phase positive helped by setting m literals true, i.e. by firing
guards.
The infrastructure already exists, and currently does the exact opposite.
--lra-theory-decisions (item 8) supplies decision literals from
cb_decide() instead of leaving them to VSIDS – but its loop skips every
atom with !prepared_atom.is_arithmetic, so it decides arithmetic atoms
first, in round-robin, and reaches the Boolean flags only once every
arithmetic atom is assigned. On this family that is precisely the worst
possible order, and it is catastrophic in measurement:
config on |
result |
|---|---|
|
3.5s, sat |
the same plus |
timeout at 90s, after 9,185,550 theory decisions |
9.2M decisions and no answer, against 3.5s. This is a falsification test of the
structural reading in The c variables are genuine don’t-cares under their own flag that came out the predicted way: forcing
the arithmetic to be decided before the flags that make it relevant is the
pathological case. It also means L1 is a small change rather than a new
mechanism – it is the same cb_decide() hook with the filter inverted, and
the existing option is a ready-made control to A/B against.
L3 – Extend the pure-theory-filter to LRA atoms. Downgraded 2026-08-05;
the original version of this paragraph was wrong. Primo has a
polarity-based relevance filter, --pure-theory-filter, which omits a theory
literal from the theory assignment when the atom occurs with only one polarity
and the solver assigned it the opposite way (EUF And Congruence Closure, from Sebastiani’s
survey §6.10).
This entry originally read the counters below as showing that three quarters of the family’s atoms were filterable, and that the mechanism did not reach LRA at all. Both readings were wrong, and the corrected analysis is in Open leads register‘s lead 1. In short:
The rule is generic and the LRA side already consumes it; the QF_LRA path simply calls an
assigned_atoms_from_backendoverload that hardcodes the mode toOff, which is a two-line change and a documented scope decision rather than a bug.A pure-positive atom is filtered only when assigned false. On a satisfiable guarded instance most are assigned true, so the 2,892 figure is not a count of filterable atoms. With the two-line change applied,
constraint-199552reports identical counters either way – 7,679 pivots and 128.4M pivot term ops – so the filter finds nothing to do here.
The counters, kept because they are still an accurate description of the family’s polarity structure, just not of the filter’s opportunity:
counter on |
value |
|---|---|
|
3,846 |
|
2,892 |
|
0 |
(:euf.pure-filter.enabled and :euf.pure-literals.filtered read 0 on any
QF_LRA file because both are set inside solve_qf_uf, which is what made them
misleading here.)
L2 – Relevancy propagation (literature-backed, larger). In DPLL(T) the SAT solver assigns every variable, including theory atoms sitting in clauses that are already satisfied by their guard; Primo then asserts those bounds into the tableau. On this family that is precisely the wasted arithmetic the unguarding experiment made visible in extreme form. z3’s answer is relevancy propagation (de Moura and Bjorner) – track which atoms are relevant to the current partial assignment and never assert the irrelevant ones. This is absent from Primo and from LRA Related Work entirely, and it targets exactly the structure documented above. It is the most principled of the three and much the largest piece of work; it should not be started before L1 and L3 have been tried, and it needs its own paper note first. L3 is its cheap static approximation and is the honest first step toward it: if filtering on polarity alone moves this family, dynamic relevance is worth the investment; if it does not, that is evidence about the family before a large mechanism is built.
Explicitly not suggested: any further guard-simplification preprocessing (closed by Unguarding is sound, and 27x slower – the guards are load-bearing), and any attempt to exploit the DAG by eager substitution – the definitions are guarded, so substitution is assignment-dependent, which is what makes L2 rather than a preprocessing pass the right framing.