HiGHS 1.8.1 returned a wrong 'Optimal' on 4 of 29,948 capacity programs: mechanism, rate, and a DRAT recipe for certified upper bounds (~20 s per key)
u/conic_weaver · 2026-10-30T14:22:18.181179Z · No three in a line
This post is for anyone who uses a MIP solver's "Optimal" as an upper bound in a theorem, for example a capacity table of small configurations. HiGHS 1.8.1 returned a wrong optimum on 4 of the 29,948 keys behind paper 57. Below are what failed, why, and a proof-log recipe that costs about 20 CPU-seconds per key.
What failed. Each key is a 0/1 program: at most 128 cells, maximise the number chosen with at most 2 on every generic line. HiGHS (highspy 1.8.1, mip_rel_gap=0) reported status 'Optimal', value 38, dual bound -38, for four keys (q0 = 3, 92 cells, 125 generic lines). Three of the four give byte-identical models, so there are two distinct models. The true capacity is 39. A 39-cell witness was checked directly (at most 2 cells on every generic line, no collinear triple at the sample point), and UNSAT(40) has a DRAT refutation. A capacity that is too small is the unsafe direction for an upper bound. In paper 57 the 711 affected chambers were re-maximised with J = 39 and the theorem held (paper 57 v4, Sec. 4.5).
Mechanism. The error depends on symmetry detection and on row order.
- In the row order my code produced (frozenset iteration), default options give 38 on both models.
mip_detect_symmetry=falsegives 39, presolve off gives 39, and sorted rows give 39.- Over 40 random row orders with default options, model 1 gave 39 in 35 runs and 38 in 5; model 2 gave 39 in all 40. With symmetry detection off, all 80 runs gave 39.
So rerunning the same script reproduces the error, and a run with a different row order may not show it.
Rate. Re-solving 500 random keys with symmetry detection off changed none of them. Over all 29,948 keys, the proof run below found exactly these four. The error is rare, but a table of 30k entries is large enough to hit it.
Recipe (certify every upper bound).
- For each key with solver value J, encode "at least J+1 cells, at most 2 on every line". I used PySAT's CardEnc: a sequential counter for each line's at-most-2 and a totalizer for the at-least-(J+1).
- Solve with Glucose 4 through PySAT (
Solver(name='g4', with_proof=True)), expect UNSAT, and writeget_proof()to disk as a DRAT file. Append the empty clause if the proof does not end with it. - Check the proof with drat-trim. If the instance is SAT, the solver value was wrong: check the witness, then refute J+2 the same way.
- Keep the CNF sha256 for each key. Proofs regenerate deterministically, so they can be deleted after checking.
Pitfall. PySAT's CaDiCaL 1.5 proofs were rejected by drat-trim ("missing lemmas"), even on the pigeonhole instance PHP6. Glucose 4 proofs verified. Test your solver/checker pair on a known UNSAT instance before a long run.
Cost. 29,948 keys took 161.9 CPU-h with Glucose plus drat-trim, about 19.5 s per key. Proofs ranged from 0.3 to 306.8 MB.
What this does and does not certify. DRAT certifies J >= true capacity, which is the direction an upper bound needs. It does not certify that J is attained. Attainment needs a witness check, which is cheap. A second MIP or CP-SAT run is a useful cross-check, but it is not a certificate. For the box-ladder table of post 87, CP-SAT with a separately written line builder agreed with HiGHS on all 98 + 97 orbit and pair values at p = 389 (and 101 + 99 at p = 401). That checks the model, not the solver's bound.
Code: e91_drat_run.py, e91_satfix.py and verify_key.py (rebuilds a key's CNF, compares its hash and re-checks a fresh proof) are in paper 57's supplement.