Skip to content

opt: validate strict optimization optima faithfully with delta-rational bounds#10059

Merged
NikolajBjorner merged 4 commits into
masterfrom
opt-strict-optima-delta-pairs
Jul 9, 2026
Merged

opt: validate strict optimization optima faithfully with delta-rational bounds#10059
NikolajBjorner merged 4 commits into
masterfrom
opt-strict-optima-delta-pairs

Conversation

@levnach

@levnach levnach commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Problem

Maximizing/minimizing under a strict inequality has a delta-rational optimum. For

(declare-const r Real)
(assert (< r 1))
(maximize r)
(check-sat)
(get-objectives)

the optimum is the supremum 1 - epsilon, but z3 reported r = 0.

The same defect makes shared-symbol objectives report a value matching neither the model nor the true optimum (issue #10028 follow-up). Minimal reproducer — a 6-mark Golomb ruler (a >32-arg distinct, so the objective is coupled to EUF) with a strict real objective obj > x5, whose true optimum is 17 + epsilon:

case before after
maximize r, r < 1 0 1 - epsilon
minimize r, r > 1 0 1 + epsilon
Golomb minimize obj, obj > x5 35/2 / 7+eps 17 + epsilon

Root cause

check_bound validates the LP hint by asserting objective >= optimum. For a supremum 1 - epsilon this is a lower bound whose value carries a negative infinitesimal (1, -1).

No lconstraint_kind can express that. The kind->infinitesimal map only yields the matching-sign cases — GT -> lower (r, +1), LT -> upper (r, -1) — or zero (GE/LE). The opposite-sign lower bound (r, -1) (i.e. r >= r0 - delta) is a relaxation that no strict inequality produces. opt_solver::mk_ge therefore projected the -epsilon away, turning r >= 1 - epsilon into the over-strong, unsatisfiable r >= 1; validation failed and the strictly smaller current model value was reported instead.

Fix — carry the infinitesimal faithfully through the bound pipeline

  • lp_api::bound gains an eps component so get_value returns the true delta value (no spurious rational fixed-variable equality is propagated to EUF).
  • lar_base_constraint stores its right-hand side as a delta-rational impq pair; rhs() returns the rational component, bound_eps() the infinitesimal one.
  • lar_solver bound activation/update threads the whole impq bound, so a lower bound (r, -1) can be asserted. constraint_holds accounts for it using the same strict-bounds delta that flattens the model, computed once per model.
  • theory_lra::mk_ge builds a fresh predicate for the (r, -1) lower bound (to avoid colliding with an already-internalized v >= r literal) and attaches eps = -1. opt_solver::mk_ge passes the unprojected value to theory_lra / theory_mi_arith / theory_inf_arith (whose bounds are already inf_rational).

The pair machinery is what makes the supremum both representable (optimum 1 - epsilon) and validatable; the reported witness model remains the flattened rational (find_delta_for_strict_bounds), consistent with the existing epsilon semantics.

Validation

  • Strict optima correct: 1-eps, 1+eps, bounded 2<r<5 -> 5-eps, and lex/box variants.
  • Integer optima and the Z3 incorrect objective with inconsistent bounds #10028 shared-symbol cases unchanged (Golomb n=6/7/8 -> 17/25/34, consistent with the model).
  • Unit tests 92/92 (release); no new debug-suite failures.
  • Opt regression corpus (73 files, model_validate=true) byte-identical to baseline.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

levnach and others added 3 commits July 7, 2026 15:30
…al bounds

Maximizing/minimizing under a strict inequality (e.g. maximize r subject to
r < 1) has the delta-rational optimum 1 - epsilon. Reporting it soundly
requires check_bound to validate the bound r >= 1 - epsilon, i.e. to assert a
LOWER bound whose value carries a NEGATIVE infinitesimal (r, -1).

No lconstraint_kind can express that: the kind->epsilon map only produces the
matching-sign infinitesimals (GT -> lower (r, +1), LT -> upper (r, -1)) or zero
(GE/LE). opt_solver::mk_ge therefore projected the negative infinitesimal away,
turning r >= 1 - epsilon into the over-strong, unsatisfiable r >= 1, so
validation spuriously failed and the strictly smaller current model value was
reported instead (e.g. r = 0). For shared-symbol objectives this also produced
values matching neither the model nor the true optimum (issue #10028 follow-up).

Fix: carry the infinitesimal faithfully through the bound pipeline.
- lp_api::bound gains an eps component so get_value reports the true delta value
  (no spurious rational fixed-variable equality is propagated to EUF).
- lar_base_constraint stores its right-hand side as a delta-rational impq pair;
  rhs() returns the rational component, bound_eps() the infinitesimal one.
- lar_solver bound activation/update threads the whole impq bound, so a lower
  bound (r, -1) can be asserted; constraint_holds accounts for it using the same
  strict-bounds delta that flattens the model (computed once per model).
- theory_lra::mk_ge builds a fresh predicate for the (r, -1) lower bound to
  avoid colliding with an already-internalized v >= r literal, and attaches
  eps = -1; opt_solver::mk_ge passes the unprojected value to theory_lra /
  theory_mi_arith / theory_inf_arith (whose bounds are inf_rational).

Results: maximize r, r<1 -> 1-eps; minimize r, r>1 -> 1+eps; bounded and lex/box
strict optima correct; integer optima and the #10028 shared-symbol cases
unchanged. Unit tests 92/92; the opt regression corpus (model_validate=true) is
byte-identical to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The nonlinear optimization path (max_with_nl) returned the linear-relaxation
optimum lp_val including its infinitesimal.  That infinitesimal is an artifact
of the strict bounds introduced by the linearization, not a genuine strict
optimum of the nonlinear problem.  Threading it into opt_solver::mk_ge made
theory_lra assert a delta-rational lower bound (r, -1) that coincides with the
column's existing strict upper bound (r, -1), fixing the objective column at a
delta value the LP core never snapped, tripping the
non_basic_columns_are_set_correctly assertion in a debug build (unit tests
max_reg / max_rev).

Project the infinitesimal away in both NLA branches (FC_DONE and FC_CONTINUE);
the rational part remains a sound bound for check_bound to validate.  The
genuinely-strict, NLA-free path (e.g. pure LP suprema and shared-symbol/EUF
objectives) is unaffected, so faithful validation of strict optima is preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… cache

The manual actions/cache step for ~/.opam was causing
OpamSystem.Internal_patch_error on macOS when a restore-key hit returned
a stale opam repo state incompatible with the current upstream.
ocaml/setup-ocaml@v3 has built-in cache management that handles this
correctly, so the manual step is removed.
@levnach levnach force-pushed the opt-strict-optima-delta-pairs branch from 51a71ac to 02a1aec Compare July 7, 2026 22:30
@levnach levnach requested a review from NikolajBjorner July 7, 2026 22:55
@levnach

levnach commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@NikolajBjorner , I think one of the main changes is here:

impq m_right_side;
. This allows to theory_lra to express something like (max x, x >= 3 - eps) in lar_solver.

Comment thread src/smt/theory_lra.cpp Outdated
if (eps.is_zero())
cT = lp().mk_var_bound(vi, kT, bound);
else
cT = lp().mk_var_bound(vi, kT, bound, eps);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplify away if-then-else to just else branch

@NikolajBjorner NikolajBjorner merged commit ed6e2a2 into master Jul 9, 2026
36 checks passed
@NikolajBjorner NikolajBjorner deleted the opt-strict-optima-delta-pairs branch July 9, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants