3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-06-11 19:35:43 +00:00

Fix NLA optimization regression and relax restore_x

- Relax restore_x() to handle backup/current size mismatches: when
  backup is shorter (new columns added), call
  move_non_basic_columns_to_bounds() to find a feasible solution.
- Fix 100x performance regression in nonlinear optimization: save LP
  optimum before check_nla and return it as bound regardless of NLA
  result, so opt_solver::check_bound() can validate via full re-solve
  with accumulated NLA lemmas.
- Refactor theory_lra::maximize() into three helpers: max_with_lp(),
  max_with_nl(), and max_result().
- Add mk_gt(theory_var, impq const&) overload for building blockers
  from saved LP optimum values.
- Add BNH multi-objective optimization test (7/7 sat in <1s vs 1/7
  in 30s before fix).
- Add restore_x test for backup size mismatch handling.

Fixes #8890

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Lev Nachmanson 2026-03-10 16:38:08 -10:00
parent bb11a56a67
commit 6d890fb026
8 changed files with 357 additions and 61 deletions

View file

@ -3983,12 +3983,86 @@ public:
return inf_eps(rational(0), inf_rational(ival.x, ival.y));
}
lp::lp_status max_with_lp(theory_var v, lpvar& vi, lp::impq& term_max) {
if (!lp().is_feasible() || lp().has_changed_columns())
make_feasible();
vi = get_lpvar(v);
auto st = lp().maximize_term(vi, term_max);
if (has_int() && lp().has_inf_int()) {
st = lp::lp_status::FEASIBLE;
lp().restore_x();
}
return st;
}
// Returns true if NLA handled the result (blocker and result are set).
// Returns false if maximize should fall through to the normal status switch.
bool max_with_nl(theory_var v, lp::lp_status& st, unsigned level, expr_ref& blocker, inf_eps& result) {
if (!m_nla || (st != lp::lp_status::OPTIMAL && st != lp::lp_status::UNBOUNDED))
return false;
// Save the LP optimum before NLA check may restore x.
auto lp_val = value(v);
auto lp_ival = get_ivalue(v);
auto nla_st = check_nla(level);
TRACE(opt, tout << "check_nla returned " << nla_st
<< " lp_ival=" << lp_ival << "\n";
if (nla_st == FC_CONTINUE) {
tout << "LP assignment at maximize optimum:\n";
for (unsigned j = 0; j < lp().column_count(); j++) {
if (!lp().get_column_value(j).is_zero())
tout << " x[" << j << "] = " << lp().get_column_value(j) << "\n";
}
});
switch (nla_st) {
case FC_DONE:
// NLA satisfied: keep the optimal assignment, return LP value
blocker = mk_gt(v);
result = lp_val;
st = lp::lp_status::FEASIBLE;
return true;
case FC_CONTINUE:
// NLA found the LP optimum violates nonlinear constraints.
// Restore x but return the LP optimum value and blocker
// as a bound for the optimizer to validate via check_bound().
lp().restore_x();
blocker = mk_gt(v, lp_ival);
result = lp_val;
st = lp::lp_status::FEASIBLE;
return true;
case FC_GIVEUP:
lp().restore_x();
st = lp::lp_status::UNBOUNDED;
return false;
}
UNREACHABLE();
return false;
}
theory_lra::inf_eps max_result(theory_var v, lpvar vi, lp::lp_status st, expr_ref& blocker, bool& has_shared) {
switch (st) {
case lp::lp_status::OPTIMAL:
init_variable_values();
TRACE(arith, display(tout << st << " v" << v << " vi: " << vi << "\n"););
blocker = mk_gt(v);
return value(v);
case lp::lp_status::FEASIBLE:
TRACE(arith, display(tout << st << " v" << v << " vi: " << vi << "\n"););
blocker = mk_gt(v);
return value(v);
default:
SASSERT(st == lp::lp_status::UNBOUNDED);
TRACE(arith, display(tout << st << " v" << v << " vi: " << vi << "\n"););
has_shared = false;
blocker = m.mk_false();
return inf_eps(rational::one(), inf_rational());
}
}
theory_lra::inf_eps maximize(theory_var v, expr_ref& blocker, bool& has_shared) {
unsigned level = 2;
lp::impq term_max;
lp::lp_status st;
lpvar vi = 0;
unsigned size_of_backup = lp().column_count();
if (has_int()) {
lp().backup_x();
}
@ -4000,57 +4074,21 @@ public:
st = lp::lp_status::UNBOUNDED;
}
else {
if (!lp().is_feasible() || lp().has_changed_columns())
make_feasible();
vi = get_lpvar(v);
st = lp().maximize_term(vi, term_max);
if (has_int() && lp().has_inf_int()) {
st = lp::lp_status::FEASIBLE;
if (lp().column_count() == size_of_backup)
lp().restore_x();
}
if (m_nla && (st == lp::lp_status::OPTIMAL || st == lp::lp_status::UNBOUNDED)) {
switch (check_nla(level)) {
case FC_DONE:
case FC_CONTINUE:
st = lp::lp_status::FEASIBLE;
break;
case FC_GIVEUP:
st = lp::lp_status::UNBOUNDED;
break;
}
if (lp().column_count() == size_of_backup)
lp().restore_x();
}
}
switch (st) {
case lp::lp_status::OPTIMAL: {
init_variable_values();
TRACE(arith, display(tout << st << " v" << v << " vi: " << vi << "\n"););
auto val = value(v);
blocker = mk_gt(v);
return val;
}
case lp::lp_status::FEASIBLE: {
auto val = value(v);
TRACE(arith, display(tout << st << " v" << v << " vi: " << vi << "\n"););
blocker = mk_gt(v);
return val;
}
default:
SASSERT(st == lp::lp_status::UNBOUNDED);
TRACE(arith, display(tout << st << " v" << v << " vi: " << vi << "\n"););
has_shared = false;
blocker = m.mk_false();
return inf_eps(rational::one(), inf_rational());
st = max_with_lp(v, vi, term_max);
inf_eps nl_result;
if (max_with_nl(v, st, level, blocker, nl_result))
return nl_result;
}
return max_result(v, vi, st, blocker, has_shared);
}
expr_ref mk_gt(theory_var v) {
lp::impq val = get_ivalue(v);
return mk_gt(v, val);
}
// Overload: create blocker from a saved impq value (used when x has been restored)
expr_ref mk_gt(theory_var v, lp::impq const& val) {
expr* obj = get_enode(v)->get_expr();
rational r = val.x;
expr_ref e(m);