3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-03-17 18:43:45 +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

@ -160,9 +160,128 @@ void test_optimize_translate() {
Z3_del_context(ctx1);
}
void test_bnh_optimize() {
// BNH multi-objective optimization problem using Z3 Optimize C API.
// Mimics /tmp/bnh_z3.py: two objectives over a constrained 2D domain.
// f1 = 4*x1^2 + 4*x2^2
// f2 = (x1-5)^2 + (x2-5)^2
// 0 <= x1 <= 5, 0 <= x2 <= 3
// C1: (x1-5)^2 + x2^2 <= 25
// C2: (x1-8)^2 + (x2+3)^2 >= 7.7
Z3_config cfg = Z3_mk_config();
Z3_context ctx = Z3_mk_context(cfg);
Z3_del_config(cfg);
Z3_sort real_sort = Z3_mk_real_sort(ctx);
Z3_ast x1 = Z3_mk_const(ctx, Z3_mk_string_symbol(ctx, "x1"), real_sort);
Z3_ast x2 = Z3_mk_const(ctx, Z3_mk_string_symbol(ctx, "x2"), real_sort);
auto mk_real = [&](int num, int den = 1) { return Z3_mk_real(ctx, num, den); };
auto mk_mul = [&](Z3_ast a, Z3_ast b) { Z3_ast args[] = {a, b}; return Z3_mk_mul(ctx, 2, args); };
auto mk_add = [&](Z3_ast a, Z3_ast b) { Z3_ast args[] = {a, b}; return Z3_mk_add(ctx, 2, args); };
auto mk_sub = [&](Z3_ast a, Z3_ast b) { Z3_ast args[] = {a, b}; return Z3_mk_sub(ctx, 2, args); };
auto mk_sq = [&](Z3_ast a) { return mk_mul(a, a); };
// f1 = 4*x1^2 + 4*x2^2
Z3_ast f1 = mk_add(mk_mul(mk_real(4), mk_sq(x1)), mk_mul(mk_real(4), mk_sq(x2)));
// f2 = (x1-5)^2 + (x2-5)^2
Z3_ast f2 = mk_add(mk_sq(mk_sub(x1, mk_real(5))), mk_sq(mk_sub(x2, mk_real(5))));
// Helper: create optimize with BNH constraints and timeout
auto mk_bnh_opt = [&]() -> Z3_optimize {
Z3_optimize opt = Z3_mk_optimize(ctx);
Z3_optimize_inc_ref(ctx, opt);
// Set timeout to 5 seconds
Z3_params p = Z3_mk_params(ctx);
Z3_params_inc_ref(ctx, p);
Z3_params_set_uint(ctx, p, Z3_mk_string_symbol(ctx, "timeout"), 5000);
Z3_optimize_set_params(ctx, opt, p);
Z3_params_dec_ref(ctx, p);
// Add BNH constraints
Z3_optimize_assert(ctx, opt, Z3_mk_ge(ctx, x1, mk_real(0)));
Z3_optimize_assert(ctx, opt, Z3_mk_le(ctx, x1, mk_real(5)));
Z3_optimize_assert(ctx, opt, Z3_mk_ge(ctx, x2, mk_real(0)));
Z3_optimize_assert(ctx, opt, Z3_mk_le(ctx, x2, mk_real(3)));
Z3_optimize_assert(ctx, opt, Z3_mk_le(ctx, mk_add(mk_sq(mk_sub(x1, mk_real(5))), mk_sq(x2)), mk_real(25)));
Z3_optimize_assert(ctx, opt, Z3_mk_ge(ctx, mk_add(mk_sq(mk_sub(x1, mk_real(8))), mk_sq(mk_add(x2, mk_real(3)))), mk_real(77, 10)));
return opt;
};
auto result_str = [](Z3_lbool r) { return r == Z3_L_TRUE ? "sat" : r == Z3_L_FALSE ? "unsat" : "unknown"; };
unsigned num_sat = 0;
// Approach 1: Minimize f1 (Python: opt.minimize(f1))
{
Z3_optimize opt = mk_bnh_opt();
Z3_optimize_minimize(ctx, opt, f1);
Z3_lbool result = Z3_optimize_check(ctx, opt, 0, nullptr);
std::cout << "BNH min f1: " << result_str(result) << std::endl;
if (result == Z3_L_TRUE) {
Z3_model m = Z3_optimize_get_model(ctx, opt);
Z3_model_inc_ref(ctx, m);
Z3_ast val; Z3_model_eval(ctx, m, f1, true, &val);
std::cout << " f1=" << Z3_ast_to_string(ctx, val) << std::endl;
Z3_model_dec_ref(ctx, m);
num_sat++;
}
Z3_optimize_dec_ref(ctx, opt);
}
// Approach 2: Minimize f2 (Python: opt2.minimize(f2))
{
Z3_optimize opt = mk_bnh_opt();
Z3_optimize_minimize(ctx, opt, f2);
Z3_lbool result = Z3_optimize_check(ctx, opt, 0, nullptr);
std::cout << "BNH min f2: " << result_str(result) << std::endl;
if (result == Z3_L_TRUE) {
Z3_model m = Z3_optimize_get_model(ctx, opt);
Z3_model_inc_ref(ctx, m);
Z3_ast val; Z3_model_eval(ctx, m, f2, true, &val);
std::cout << " f2=" << Z3_ast_to_string(ctx, val) << std::endl;
Z3_model_dec_ref(ctx, m);
num_sat++;
}
Z3_optimize_dec_ref(ctx, opt);
}
// Approach 3: Weighted sum method (Python loop over weights)
int weights[][2] = {{1, 4}, {2, 3}, {1, 1}, {3, 2}, {4, 1}};
for (auto& w : weights) {
Z3_optimize opt = mk_bnh_opt();
Z3_ast weighted = mk_add(mk_mul(mk_real(w[0], 100), f1), mk_mul(mk_real(w[1], 100), f2));
Z3_optimize_minimize(ctx, opt, weighted);
Z3_lbool result = Z3_optimize_check(ctx, opt, 0, nullptr);
std::cout << "BNH weighted (w1=" << w[0] << "/5, w2=" << w[1] << "/5): "
<< result_str(result) << std::endl;
if (result == Z3_L_TRUE) {
Z3_model m = Z3_optimize_get_model(ctx, opt);
Z3_model_inc_ref(ctx, m);
Z3_ast v1, v2;
Z3_model_eval(ctx, m, f1, true, &v1);
Z3_model_eval(ctx, m, f2, true, &v2);
std::cout << " f1=" << Z3_ast_to_string(ctx, v1)
<< " f2=" << Z3_ast_to_string(ctx, v2) << std::endl;
Z3_model_dec_ref(ctx, m);
num_sat++;
}
Z3_optimize_dec_ref(ctx, opt);
}
std::cout << "BNH: " << num_sat << "/7 optimizations returned sat" << std::endl;
Z3_del_context(ctx);
std::cout << "BNH optimization test done" << std::endl;
}
void tst_api() {
test_apps();
test_bvneg();
test_mk_distinct();
test_optimize_translate();
test_bnh_optimize();
}
void tst_bnh_opt() {
test_bnh_optimize();
}

View file

@ -564,6 +564,7 @@ void setup_args_parser(argument_parser &parser) {
"test rationals using plus instead of +=");
parser.add_option_with_help_string("--maximize_term", "test maximize_term()");
parser.add_option_with_help_string("--patching", "test patching");
parser.add_option_with_help_string("--restore_x", "test restore_x");
}
struct fff {
@ -1765,6 +1766,124 @@ void test_gomory_cut() {
void test_nla_order_lemma() { nla::test_order_lemma(); }
void test_restore_x() {
std::cout << "testing restore_x" << std::endl;
// Test 1: backup shorter than current (new variables added after backup)
{
lar_solver solver;
lpvar x = solver.add_var(0, false);
lpvar y = solver.add_var(1, false);
solver.add_var_bound(x, GE, mpq(0));
solver.add_var_bound(x, LE, mpq(10));
solver.add_var_bound(y, GE, mpq(0));
solver.add_var_bound(y, LE, mpq(10));
vector<std::pair<mpq, lpvar>> coeffs;
coeffs.push_back({mpq(1), x});
coeffs.push_back({mpq(1), y});
unsigned t = solver.add_term(coeffs, 2);
solver.add_var_bound(t, GE, mpq(3));
solver.add_var_bound(t, LE, mpq(15));
auto status = solver.solve();
SASSERT(status == lp_status::OPTIMAL);
// Backup the current solution
solver.backup_x();
// Add a new variable with bounds, making the system larger
lpvar z = solver.add_var(3, false);
solver.add_var_bound(z, GE, mpq(1));
solver.add_var_bound(z, LE, mpq(5));
// restore_x should detect backup < current and call move_non_basic_columns_to_bounds
solver.restore_x();
// The solver should find a feasible solution
status = solver.get_status();
SASSERT(status == lp_status::OPTIMAL || status == lp_status::FEASIBLE);
std::cout << " test 1 (backup shorter): " << lp_status_to_string(status) << " - PASSED" << std::endl;
}
// Test 2: backup longer than current (columns removed after backup, or pop)
{
lar_solver solver;
lpvar x = solver.add_var(0, false);
lpvar y = solver.add_var(1, false);
solver.add_var_bound(x, GE, mpq(0));
solver.add_var_bound(x, LE, mpq(10));
solver.add_var_bound(y, GE, mpq(0));
solver.add_var_bound(y, LE, mpq(10));
vector<std::pair<mpq, lpvar>> coeffs;
coeffs.push_back({mpq(1), x});
coeffs.push_back({mpq(1), y});
unsigned t = solver.add_term(coeffs, 2);
solver.add_var_bound(t, GE, mpq(2));
// Add more variables to make backup larger
lpvar z = solver.add_var(3, false);
solver.add_var_bound(z, GE, mpq(0));
solver.add_var_bound(z, LE, mpq(5));
auto status = solver.solve();
(void)status;
SASSERT(status == lp_status::OPTIMAL);
// Backup with the full system
solver.backup_x();
// restore_x with same-size backup should work fine
solver.restore_x();
std::cout << " test 2 (same size backup): PASSED" << std::endl;
}
// Test 3: move_non_basic_columns_to_bounds after solve
{
lar_solver solver;
lpvar x = solver.add_var(0, false);
lpvar y = solver.add_var(1, false);
solver.add_var_bound(x, GE, mpq(1));
solver.add_var_bound(x, LE, mpq(10));
solver.add_var_bound(y, GE, mpq(1));
solver.add_var_bound(y, LE, mpq(10));
auto status = solver.solve();
SASSERT(status == lp_status::OPTIMAL);
// Add new constraint: x + y >= 5
vector<std::pair<mpq, lpvar>> coeffs;
coeffs.push_back({mpq(1), x});
coeffs.push_back({mpq(1), y});
unsigned t = solver.add_term(coeffs, 2);
solver.add_var_bound(t, GE, mpq(5));
solver.add_var_bound(t, LE, mpq(15));
// Add another variable
lpvar w = solver.add_var(3, false);
solver.add_var_bound(w, GE, mpq(2));
solver.add_var_bound(w, LE, mpq(8));
// Solve expanded system, then move non-basic columns to bounds
status = solver.solve();
SASSERT(status == lp_status::OPTIMAL);
solver.move_non_basic_columns_to_bounds();
status = solver.get_status();
SASSERT(status == lp_status::OPTIMAL || status == lp_status::FEASIBLE);
// Verify the model satisfies the constraints
std::unordered_map<lpvar, mpq> model;
solver.get_model(model);
SASSERT(model[x] >= mpq(1) && model[x] <= mpq(10));
SASSERT(model[y] >= mpq(1) && model[y] <= mpq(10));
SASSERT(model[w] >= mpq(2) && model[w] <= mpq(8));
std::cout << " test 3 (move_non_basic_columns_to_bounds): " << lp_status_to_string(status) << " - PASSED" << std::endl;
}
std::cout << "restore_x tests passed" << std::endl;
}
void test_lp_local(int argn, char **argv) {
// initialize_util_module();
// initialize_numerics_module();
@ -1792,6 +1911,10 @@ void test_lp_local(int argn, char **argv) {
test_patching();
return finalize(0);
}
if (args_parser.option_is_used("--restore_x")) {
test_restore_x();
return finalize(0);
}
if (args_parser.option_is_used("-nla_cn")) {
#ifdef Z3DEBUG
nla::test_cn();

View file

@ -175,6 +175,7 @@ int main(int argc, char ** argv) {
TST(var_subst);
TST(simple_parser);
TST(api);
TST(bnh_opt);
TST(api_algebraic);
TST(api_polynomial);
TST(api_pb);