3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-04-15 08:44:10 +00:00
This commit is contained in:
Arie 2026-04-12 09:41:57 -07:00 committed by GitHub
commit 7ddd8d5ab5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 200 additions and 4 deletions

View file

@ -99,12 +99,122 @@ bool horner::lemmas_on_row(const T& row) {
}
// Find the binary monomial y*v in emonics, return its variable or null_lpvar.
static lpvar find_binary_monic(emonics const& emons, lpvar y, lpvar v) {
if (!emons.is_used_in_monic(v))
return null_lpvar;
for (auto const& m : emons.get_use_list(v))
if (m.size() == 2 && (m.vars()[0] == y || m.vars()[1] == y))
return m.var();
return null_lpvar;
}
// Recover named intermediates destroyed by solve-eqs.
//
// When solve-eqs eliminates x = sum(c_i * v_i), product x*y splits into
// sum(c_i * v_i * y). Horner's cross-nested factoring recovers y*sum(c_i*v_i)
// and interval_from_term finds the LP term column tc := sum(c_i * v_i)
// with tight bounds [L, U].
//
// We do two things:
// 1. Create monomial m := y*tc, add LP row m - sum(c_i * mon(y,v_i)) = 0
// 2. If variable x with monomial x*y exists and val(x) = val(tc),
// propagate: tc in [L,U] => x in [L,U]
void horner::introduce_monomials_from_term_columns() {
if (c().params().arith_nl_horner_max_new_monomials() == 0)
return;
auto const& term_cols = c().m_intervals.term_columns();
if (term_cols.empty())
return;
unsigned added = 0;
for (lpvar tc : term_cols) {
if (!c().lra.column_has_lower_bound(tc) || !c().lra.column_has_upper_bound(tc))
continue;
if (!c().lra.column_has_term(tc))
continue;
auto const& term = c().lra.get_term(tc);
for (auto const& ti : term) {
lpvar vi = ti.j();
if (!c().m_emons.is_used_in_monic(vi))
continue;
for (auto const& m : c().m_emons.get_use_list(vi)) {
if (m.size() != 2)
continue;
lpvar y = (m.vars()[0] == vi) ? m.vars()[1] : m.vars()[0];
auto key = std::make_pair(std::min(y, tc), std::max(y, tc));
if (m_introduced_monomials.contains(key))
continue;
// Check that mon(y, v_i) exists for every v_i in tc
lp::lar_term eq_term;
bool complete = true;
for (auto const& tj : term) {
lpvar yv = find_binary_monic(c().m_emons, y, tj.j());
if (yv == null_lpvar) { complete = false; break; }
eq_term.add_monomial(-tj.coeff(), yv);
}
if (!complete)
continue;
m_introduced_monomials.push_back(key);
// (1) m := y * tc, with LP row: m - sum(c_i * mon(y,v_i)) = 0
lpvar factors[2] = { y, tc };
lpvar new_mon = c().add_mul_def(2, factors);
eq_term.add_monomial(rational::one(), new_mon);
lp::lpvar eq_col = c().lra.add_term(eq_term.coeffs_as_vector(), UINT_MAX);
c().lra.update_column_type_and_bound(eq_col, llc::EQ, rational::zero(), nullptr);
c().m_check_feasible = true;
TRACE(nla_solver, tout << "introduced monomial j" << new_mon
<< " := j" << y << " * j" << tc << "\n";);
// (2) Propagate tc's bounds to variable x where mon(x, y) exists
// and val(x) = val(tc), i.e., x equals tc in current model.
for (auto const& m2 : c().m_emons.get_use_list(y)) {
if (m2.size() != 2)
continue;
lpvar x = (m2.vars()[0] == y) ? m2.vars()[1] : m2.vars()[0];
if (x == tc || c().lra.column_has_term(x))
continue;
if (c().lra.get_column_value(x) != c().lra.get_column_value(tc))
continue;
if (c().lra.column_has_lower_bound(tc)) {
c().lra.update_column_type_and_bound(
x, llc::GE, c().lra.get_lower_bound(tc).x,
c().lra.get_column_lower_bound_witness(tc));
c().m_check_feasible = true;
TRACE(nla_solver, tout << "bound j" << x << " >= "
<< c().lra.get_lower_bound(tc).x << " from j" << tc << "\n";);
}
if (c().lra.column_has_upper_bound(tc)) {
c().lra.update_column_type_and_bound(
x, llc::LE, c().lra.get_upper_bound(tc).x,
c().lra.get_column_upper_bound_witness(tc));
c().m_check_feasible = true;
TRACE(nla_solver, tout << "bound j" << x << " <= "
<< c().lra.get_upper_bound(tc).x << " from j" << tc << "\n";);
}
}
if (++added >= c().params().arith_nl_horner_max_new_monomials())
return;
}
}
}
}
bool horner::horner_lemmas() {
if (!c().params().arith_nl_horner()) {
TRACE(nla_solver, tout << "not generating horner lemmas\n";);
return false;
}
c().lp_settings().stats().m_horner_calls++;
c().m_intervals.clear_term_columns();
const auto& matrix = c().lra.A_r();
// choose only rows that depend on m_to_refine variables
std::set<unsigned> rows_to_check;
@ -129,6 +239,10 @@ bool horner::horner_lemmas() {
conflict = true;
}
}
if (!conflict)
introduce_monomials_from_term_columns();
return conflict;
}
}

View file

@ -31,7 +31,8 @@ class core;
class horner : common {
nex_creator::sum_factory m_row_sum;
unsigned m_row_index;
unsigned m_row_index;
svector<std::pair<lpvar, lpvar>> m_introduced_monomials; // track what we've already created
public:
typedef intervals::interval interv;
horner(core *core);
@ -49,5 +50,6 @@ public:
template <typename T> // T has an iterator of (coeff(), var())
bool row_has_monomial_to_refine(const T&) const;
bool interval_from_term_with_deps(const nex* e, intervals::interval&) const;
void introduce_monomials_from_term_columns();
}; // end of horner
}

View file

@ -36,7 +36,12 @@ namespace lp {
struct term_comparer {
bool operator()(const lar_term& a, const lar_term& b) const {
return a == b;
if (a.size() != b.size()) return false;
for (const auto& p : a) {
auto const* e = b.coeffs().find_core(p.j());
if (!e || e->get_data().m_value != p.coeff()) return false;
}
return true;
}
};

View file

@ -292,6 +292,7 @@ bool intervals::interval_from_term(const nex& e, scoped_dep_interval& i) {
if (j + 1 == 0)
return false;
m_term_columns.push_back(j);
set_var_interval<wd>(j, i);
interval bi;
m_dep_intervals.mul<wd>(a, i, bi);

View file

@ -21,7 +21,8 @@ class core;
class intervals {
mutable dep_intervals m_dep_intervals;
core* m_core;
svector<lpvar> m_term_columns; // LP columns found by interval_from_term
public:
typedef dep_intervals::interval interval;
private:
@ -88,5 +89,8 @@ public:
std::ostream& display_separating_interval(std::ostream& out, const nex*n, const scoped_dep_interval& interv_wd, u_dependency* initial_deps);
bool conflict_u_l(const interval& a, const interval& b) const;
void clear_term_columns() { m_term_columns.clear(); }
const svector<lpvar>& term_columns() const { return m_term_columns; }
}; // end of intervals
} // end of namespace nla

View file

@ -92,6 +92,7 @@ def_module_params(module_name='smt',
('arith.nl.propagate_linear_monomials', BOOL, True, 'propagate linear monomials'),
('arith.nl.optimize_bounds', BOOL, True, 'enable bounds optimization'),
('arith.nl.cross_nested', BOOL, True, 'enable cross-nested consistency checking'),
('arith.nl.horner_max_new_monomials', UINT, 2, 'maximum number of new monomials introduced per Horner round when a shared factor with a bounded linear combination is discovered (0 to disable)'),
('arith.nl.log', BOOL, False, 'Log lemmas sent to nra solver'),
('arith.propagate_eqs', BOOL, True, 'propagate (cheap) equalities'),
('arith.epsilon', DOUBLE, 1.0, 'initial value of epsilon used for model generation of infinitesimals'),

View file

@ -207,13 +207,82 @@ void test_nla_intervals_fractional() {
VERIFY(true); // Placeholder
}
void test_fetch_normalized_term_column() {
std::cout << "test_fetch_normalized_term_column\n";
lp::lar_solver s;
// Create some variables
lpvar x = s.add_var(0, true); // j0
lpvar y = s.add_var(1, true); // j1
lpvar z = s.add_var(2, true); // j2
// Add a term t = 2*x + 3*y and register it
lp::lar_term t;
t.add_monomial(rational(2), x);
t.add_monomial(rational(3), y);
s.add_term(t.coeffs_as_vector(), UINT_MAX);
s.register_existing_terms();
// Now build the same term independently and look it up
lp::lar_term query;
query.add_monomial(rational(2), x);
query.add_monomial(rational(3), y);
lp::mpq a;
lp::lar_term norm_query = query.get_normalized_by_min_var(a);
std::pair<lp::mpq, lpvar> result;
bool found = s.fetch_normalized_term_column(norm_query, result);
VERIFY(found);
std::cout << " round-trip lookup: " << (found ? "PASS" : "FAIL") << "\n";
// Build query with variables added in reverse order
lp::lar_term query_rev;
query_rev.add_monomial(rational(3), y);
query_rev.add_monomial(rational(2), x);
lp::lar_term norm_rev = query_rev.get_normalized_by_min_var(a);
bool found_rev = s.fetch_normalized_term_column(norm_rev, result);
VERIFY(found_rev);
std::cout << " reverse-order lookup: " << (found_rev ? "PASS" : "FAIL") << "\n";
// Test a 3-variable term: x - y + 5*z
lp::lar_term t2;
t2.add_monomial(rational(1), x);
t2.add_monomial(rational(-1), y);
t2.add_monomial(rational(5), z);
s.add_term(t2.coeffs_as_vector(), UINT_MAX);
s.register_existing_terms();
lp::lar_term query2;
query2.add_monomial(rational(1), x);
query2.add_monomial(rational(-1), y);
query2.add_monomial(rational(5), z);
lp::lar_term norm2 = query2.get_normalized_by_min_var(a);
found = s.fetch_normalized_term_column(norm2, result);
VERIFY(found);
std::cout << " 3-variable term lookup: " << (found ? "PASS" : "FAIL") << "\n";
// Test that a non-registered term is NOT found
lp::lar_term query3;
query3.add_monomial(rational(7), x);
query3.add_monomial(rational(11), y);
lp::lar_term norm3 = query3.get_normalized_by_min_var(a);
bool found_missing = s.fetch_normalized_term_column(norm3, result);
VERIFY(!found_missing);
std::cout << " non-existent term not found: " << (!found_missing ? "PASS" : "FAIL") << "\n";
}
void test_nla_intervals() {
test_nla_intervals_basic();
test_nla_intervals_negative();
test_nla_intervals_negative();
test_nla_intervals_zero_crossing();
test_nla_intervals_power();
test_nla_intervals_mixed_signs();
test_nla_intervals_fractional();
test_fetch_normalized_term_column();
}
} // namespace nla