mirror of
https://github.com/Z3Prover/z3
synced 2026-07-21 06:25:50 +00:00
opt: validate strict optimization optima faithfully with delta-rational bounds (#10059)
## Problem Maximizing/minimizing under a **strict** inequality has a delta-rational optimum. For ```smt2 (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 #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> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Nikolaj Bjorner <nbjorner@microsoft.com>
This commit is contained in:
parent
5a55ed7cfb
commit
ed6e2a241d
7 changed files with 212 additions and 84 deletions
|
|
@ -3304,6 +3304,16 @@ public:
|
|||
}
|
||||
|
||||
api_bound* mk_var_bound(bool_var bv, theory_var v, lp_api::bound_kind bk, rational const& bound) {
|
||||
return mk_var_bound(bv, v, bk, bound, rational::zero());
|
||||
}
|
||||
|
||||
// eps is the infinitesimal coefficient of the asserted (positive-literal)
|
||||
// bound value: the bound means v (>=|<=) bound + eps*delta. Non-zero only
|
||||
// for the delta-rational bounds that faithfully validate strict
|
||||
// optimization optima (a maximize supremum r - delta becomes a lower bound
|
||||
// (r, -1)). Only the asserted direction (cT) carries eps; the negation cF
|
||||
// is never activated on the optimization validation path.
|
||||
api_bound* mk_var_bound(bool_var bv, theory_var v, lp_api::bound_kind bk, rational const& bound, rational const& eps) {
|
||||
scoped_internalize_state st(*this);
|
||||
st.vars().push_back(v);
|
||||
st.coeffs().push_back(rational::one());
|
||||
|
|
@ -3315,7 +3325,7 @@ public:
|
|||
lp::lconstraint_kind kT = bound2constraint_kind(v_is_int, bk, true);
|
||||
lp::lconstraint_kind kF = bound2constraint_kind(v_is_int, bk, false);
|
||||
|
||||
cT = lp().mk_var_bound(vi, kT, bound);
|
||||
cT = lp().mk_var_bound(vi, kT, bound, eps);
|
||||
if (v_is_int) {
|
||||
rational boundF = (bk == lp_api::lower_t) ? bound - 1 : bound + 1;
|
||||
cF = lp().mk_var_bound(vi, kF, boundF);
|
||||
|
|
@ -3326,7 +3336,7 @@ public:
|
|||
add_ineq_constraint(cT, literal(bv, false));
|
||||
add_ineq_constraint(cF, literal(bv, true));
|
||||
|
||||
return alloc(api_bound, literal(bv, false), v, vi, v_is_int, bound, bk, cT, cF);
|
||||
return alloc(api_bound, literal(bv, false), v, vi, v_is_int, bound, bk, cT, cF, eps);
|
||||
}
|
||||
|
||||
//
|
||||
|
|
@ -4055,11 +4065,22 @@ public:
|
|||
tout << " x[" << j << "] = " << lp().get_column_value(j) << "\n";
|
||||
}
|
||||
});
|
||||
// Discard the infinitesimal of the value returned from the NLA path.
|
||||
// When NLA is involved the objective is nonlinear, so lp_val is the
|
||||
// optimum of the LINEAR relaxation: its infinitesimal comes from the
|
||||
// strict bounds introduced by the linearization, not from a genuine
|
||||
// strict optimum of the nonlinear problem. If it were kept,
|
||||
// opt_solver::mk_ge would assert a delta-rational bound (r, -1) that the
|
||||
// real problem cannot honor, fixing the objective column at a delta
|
||||
// value the LP core cannot snap on the next solve (assertion
|
||||
// non_basic_columns_are_set_correctly). The rational part remains a
|
||||
// sound bound for the optimizer to validate via check_bound.
|
||||
inf_eps lp_val_no_eps(lp_val.get_infinity(), inf_rational(lp_val.get_rational()));
|
||||
switch (nla_st) {
|
||||
case FC_DONE:
|
||||
// NLA satisfied: keep the optimal assignment, return LP value
|
||||
blocker = mk_gt(v);
|
||||
result = lp_val;
|
||||
result = lp_val_no_eps;
|
||||
st = lp::lp_status::FEASIBLE;
|
||||
return true;
|
||||
case FC_CONTINUE:
|
||||
|
|
@ -4068,7 +4089,7 @@ public:
|
|||
// as a bound for the optimizer to validate via check_bound().
|
||||
lp().restore_x();
|
||||
blocker = mk_gt(v, lp_ival);
|
||||
result = lp_val;
|
||||
result = lp_val_no_eps;
|
||||
st = lp::lp_status::FEASIBLE;
|
||||
return true;
|
||||
case FC_GIVEUP:
|
||||
|
|
@ -4249,11 +4270,32 @@ public:
|
|||
|
||||
expr_ref mk_ge(generic_model_converter& fm, theory_var v, inf_rational const& val) {
|
||||
rational r = val.get_rational();
|
||||
bool is_strict = val.get_infinitesimal().is_pos();
|
||||
bool is_strict = val.get_infinitesimal().is_pos();
|
||||
// A negative infinitesimal encodes a delta-rational lower bound
|
||||
// v >= r - delta. It arises when validating a strict maximization
|
||||
// optimum (supremum r reported as r - epsilon): no lconstraint_kind
|
||||
// yields a lower bound with a -delta component, so it is threaded
|
||||
// through as an explicit eps on the bound (see lp_api::bound,
|
||||
// lar_solver::mk_var_bound). Over the reals this is a genuine bound
|
||||
// (feasible together with the problem's own strict bound v <= r - delta,
|
||||
// fixing v = r - delta), which is exactly what makes the supremum
|
||||
// achievable in the delta field and lets check_bound validate it.
|
||||
bool is_lower_eps = val.get_infinitesimal().is_neg();
|
||||
app_ref b(m);
|
||||
bool is_int = a.is_int(get_expr(v));
|
||||
TRACE(arith, display(tout << "v" << v << "\n"););
|
||||
if (is_strict) {
|
||||
if (is_lower_eps) {
|
||||
// Fresh, dedicated predicate for the delta-rational lower bound
|
||||
// v >= r - delta. A plain (a.mk_ge v r) atom would collide with an
|
||||
// already-internalized 'v >= r' literal (e.g. from the problem's own
|
||||
// strict bound v < r), which carries no infinitesimal and would make
|
||||
// validation assert the over-strong v >= r. The bound's real meaning
|
||||
// (including the -delta) is attached via the api_bound's eps below.
|
||||
std::ostringstream strm;
|
||||
strm << r << " - eps <= " << mk_pp(get_expr(v), m) << " (opt)";
|
||||
b = m.mk_const(symbol(strm.str()), m.mk_bool_sort());
|
||||
}
|
||||
else if (is_strict) {
|
||||
b = a.mk_le(mk_obj(v), a.mk_numeral(r, is_int));
|
||||
}
|
||||
else {
|
||||
|
|
@ -4267,7 +4309,8 @@ public:
|
|||
// ctx().set_enode_flag(bv, true);
|
||||
lp_api::bound_kind bkind = lp_api::bound_kind::lower_t;
|
||||
if (is_strict) bkind = lp_api::bound_kind::upper_t;
|
||||
api_bound* a = mk_var_bound(bv, v, bkind, r);
|
||||
rational eps = is_lower_eps ? rational::minus_one() : rational::zero();
|
||||
api_bound* a = mk_var_bound(bv, v, bkind, r, eps);
|
||||
mk_bound_axioms(*a);
|
||||
updt_unassigned_bounds(v, +1);
|
||||
m_bounds[v].push_back(a);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue