#include "nlsat/levelwise.h" #include "nlsat/nlsat_types.h" #include "nlsat/nlsat_assignment.h" #include #include #include #include #include #include "math/polynomial/algebraic_numbers.h" #include "nlsat_common.h" namespace nlsat { // Local enums reused from previous scaffolding enum class property_mapping_case : unsigned { case1 = 0, case2 = 1, case3 = 2 }; enum class prop_enum : unsigned { ir_ord, an_del, non_null, ord_inv_reducible, ord_inv_irreducible, sgn_inv_reducible, sgn_inv_irreducible, connected, an_sub, sample, repr, holds, _count }; unsigned prop_count() { return static_cast(prop_enum::_count);} // no score-based ordering; precedence is defined by m_p_relation only struct levelwise::impl { struct property { prop_enum prop; poly* p = nullptr; unsigned s_idx = 0; // index of the root function, if applicable unsigned level = 0; }; solver& m_solver; polynomial_ref_vector const& m_P; var m_n; pmanager& m_pm; anum_manager& m_am; std::vector m_Q; // the set of properties to prove bool m_fail = false; // Property precedence relation stored as pairs (lesser, greater) std::vector> m_p_relation; // Transitive closure matrix: dom[a][b] == true iff a ▹ b (a strictly dominates b). // Invert edges when populating dom: greater ▹ lesser. std::vector> m_prop_dom; assignment const & sample() const { return m_solver.sample();} assignment & sample() { return m_solver.sample(); } // max_x plays the role of n in algorith 1 of the levelwise paper. impl(solver& solver, polynomial_ref_vector const& ps, var max_x, assignment const& s, pmanager& pm, anum_manager& am) : m_solver(solver), m_P(ps), m_n(max_x), m_pm(pm), m_am(am) { TRACE(levelwise, tout << "m_n:" << m_n << "\n";); init_property_relation(); } #ifdef Z3DEBUG bool check_prop_init() { for (unsigned k = 0; k < prop_count(); ++k) if (m_prop_dom[k][k]) return false; return !dominates(prop_enum::ord_inv_irreducible, prop_enum::non_null); } #endif void init_property_relation() { m_p_relation.clear(); auto add = [&](prop_enum lesser, prop_enum greater) { m_p_relation.emplace_back(lesser, greater); }; // m_p_relation stores edges (lesser -> greater). // The edges below follow Figure 8. Examples include: an_del -> ir_ord, sample -> ir_ord. add(prop_enum::holds, prop_enum::repr); add(prop_enum::repr, prop_enum::sample); add(prop_enum::sample, prop_enum::an_sub); add(prop_enum::an_sub, prop_enum::connected); // connected < sgn_inv_* add(prop_enum::connected, prop_enum::sgn_inv_reducible); add(prop_enum::connected, prop_enum::sgn_inv_irreducible); // sgn_inv_* < ord_inv_* (same reducibility) add(prop_enum::sgn_inv_reducible, prop_enum::ord_inv_reducible); add(prop_enum::sgn_inv_irreducible, prop_enum::ord_inv_irreducible); // ord_inv_* < non_null add(prop_enum::ord_inv_reducible, prop_enum::non_null); add(prop_enum::ord_inv_irreducible, prop_enum::non_null); // non_null < an_del add(prop_enum::non_null, prop_enum::an_del); // an_del < ir_ord add(prop_enum::an_del, prop_enum::ir_ord); // Additional explicit edge from Fig 8 add(prop_enum::sample, prop_enum::ir_ord); // Build transitive closure matrix for quick comparisons unsigned N = prop_count(); m_prop_dom.assign(N, std::vector(N, false)); for (auto const& pr : m_p_relation) { unsigned lesser = static_cast(pr.first); unsigned greater = static_cast(pr.second); // Build dominance relation as: greater ▹ lesser m_prop_dom[greater][lesser] = true; } // Floyd-Warshall style closure on a tiny fixed-size set for (unsigned k = 0; k < N; ++k) for (unsigned i = 0; i < N; ++i) if (m_prop_dom[i][k]) for (unsigned j = 0; j < N; ++j) if (m_prop_dom[k][j]) m_prop_dom[i][j] = true; #ifdef Z3DEBUG SASSERT(check_prop_init()); #endif } unsigned max_var(poly* p) { return m_pm.max_var(p); } std::vector seed_properties() { std::vector Q; // Algorithm 1: initial goals are sgn_inv(p, s) for p in ps at current level of max_x for (unsigned i = 0; i < m_P.size(); ++i) { poly* p = m_P.get(i); Q.push_back(property{ prop_enum::sgn_inv_irreducible, p, /*s_idx*/0, /* level */ m_n - 1}); } return Q; } struct result_struct { symbolic_interval I; // Set E of indexed root expressions at level i for P_non_null: the root functions from E pass throug s[i] std::vector E; // Initial ordering buckets for omega: each bucket groups equal-valued roots std::vector> omega_buckets; std::vector Q; bool fail = false; }; // Bucket of equal-valued roots used for initial omega ordering struct bucket_t { scoped_anum val; std::vector members; bucket_t(anum_manager& am): val(am) {} bucket_t(bucket_t&& other) noexcept : val(other.val.m()), members(std::move(other.members)) { val = other.val; } bucket_t(bucket_t const&) = delete; bucket_t& operator=(bucket_t const&) = delete; }; // Internal carrier to keep root value paired with indexed root expr struct root_item_t { scoped_anum val; indexed_root_expr ire; root_item_t(anum_manager& am, poly* p, unsigned idx, anum const& v) : val(am), ire{ p, idx } { am.set(val, v); } root_item_t(root_item_t&& other) noexcept : val(other.val.m()), ire(other.ire) { val = other.val; } root_item_t(root_item_t const&) = delete; root_item_t& operator=(root_item_t const&) = delete; }; bool dominates(const property& a, const property& b) const { return a.p == b.p && dominates(a.prop, b.prop); } bool dominates(prop_enum a, prop_enum b) const { return m_prop_dom[static_cast(a)][static_cast(b)]; } // Pretty-print helpers static const char* prop_name(prop_enum p) { switch (p) { case prop_enum::ir_ord: return "ir_ord"; case prop_enum::an_del: return "an_del"; case prop_enum::non_null: return "non_null"; case prop_enum::ord_inv_reducible: return "ord_inv_reducible"; case prop_enum::ord_inv_irreducible: return "ord_inv_irreducible"; case prop_enum::sgn_inv_reducible: return "sgn_inv_reducible"; case prop_enum::sgn_inv_irreducible: return "sgn_inv_irreducible"; case prop_enum::connected: return "connected"; case prop_enum::an_sub: return "an_sub"; case prop_enum::sample: return "sample"; case prop_enum::repr: return "repr"; case prop_enum::holds: return "holds"; case prop_enum::_count: return "_count"; } return "?"; } std::ostream& display(std::ostream& out, const property & pr) { out << "{prop:" << prop_name(pr.prop) << ", level:" << pr.level << ", s_idx:" << pr.s_idx; if (pr.p) { out << ", p:"; ::nlsat::display(out, m_solver, polynomial_ref(pr.p, m_pm)); } else { out << ", p:null"; } out << "}"; return out; } std::vector greatest_to_refine(unsigned level, prop_enum prop_to_avoid) { // Collect candidates on current level, excluding sgn_inv_irreducible std::vector cand; cand.reserve(m_Q.size()); for (const auto& q : m_Q) if (q.level == level && q.prop != prop_to_avoid) cand.push_back(q); if (cand.empty()) return {}; // Determine maxima w.r.t. ▹ using the transitive closure matrix // Dominance requires the same polynomial in both compared properties std::vector dominated(cand.size(), false); for (size_t i = 0; i < cand.size(); ++i) { for (size_t j = 0; j < cand.size(); ++j) { if (i != j && dominates(cand[j], cand[i])) { dominated[i] = true; break; } } } // Extract non-dominated (greatest) candidates; keep deterministic order by (poly id, prop enum) struct Key { unsigned pid; unsigned pprop; size_t idx; }; std::vector keys; keys.reserve(cand.size()); for (size_t i = 0; i < cand.size(); ++i) { if (!dominated[i]) { keys.push_back(Key{ polynomial::manager::id(cand[i].p), static_cast(cand[i].prop), i }); } } std::sort(keys.begin(), keys.end(), [](Key const& a, Key const& b){ if (a.pid != b.pid) return a.pid < b.pid; return a.pprop < b.pprop; }); std::vector ret; ret.reserve(keys.size()); for (auto const& k : keys) ret.push_back(cand[k.idx]); return ret; } // check that at least one coeeficient of p \in Q[x0,..., x_{i-1}](x) does not evaluate to zere at the sample. bool poly_is_not_nullified_at_sample_at_level(poly* p, unsigned i) { // iterate coefficients of p with respect to the current level variable m_i unsigned deg = m_pm.degree(p, i); polynomial_ref c(m_pm); for (unsigned j = 0; j <= deg; ++j) { c = m_pm.coeff(p, i, j); if (sign(c, sample(), m_am) != 0) return true; } return false; } // Step 1a: collect E and root values void collect_E_and_roots(std::vector const& P_non_null, unsigned i, std::vector& E, std::vector& roots_out) { var y = i; for (auto const* p0 : P_non_null) { auto* p = const_cast(p0); if (m_pm.max_var(p) != y) continue; scoped_anum_vector roots(m_am); m_am.isolate_roots(polynomial_ref(p, m_pm), undef_var_assignment(sample(), y), roots); unsigned num_roots = roots.size(); TRACE(levelwise, tout << "roots (" << num_roots << "):"; for (unsigned kk = 0; kk < num_roots; ++kk) { tout << " "; m_am.display(tout, roots[kk]); } tout << std::endl; ); for (unsigned k = 0; k < num_roots; ++k) { E.push_back(indexed_root_expr{ p, k + 1 }); roots_out.emplace_back(m_am, p, k + 1, roots[k]); } } } // Find an existing bucket that has the same algebraic value; returns nullptr if none found bucket_t* find_bucket_by_value(anum const& v, std::vector>& buckets) { for (auto& b : buckets) if (m_am.compare(v, b->val) == 0) return b.get(); return nullptr; } // Append a root to a given bucket (does not change the bucket's value) void add_root_to_bucket(bucket_t& bucket, root_item_t const& r) { bucket.members.push_back(r.ire); } // Step 1b: bucketize roots by equality of values void add_root_to_buckets(root_item_t const& r, std::vector>& buckets) { if (auto* b = find_bucket_by_value(r.val, buckets)) { add_root_to_bucket(*b, r); return; } auto nb = std::make_unique(m_am); m_am.set(nb->val, r.val); add_root_to_bucket(*nb, r); buckets.push_back(std::move(nb)); } void bucketize_roots(std::vector const& roots, std::vector>& buckets) { for (auto const& r : roots) add_root_to_buckets(r, buckets); } // Step 2a: sort buckets and form omega_buckets void build_omega_buckets(std::vector>& buckets, std::vector>& omega_buckets) { std::sort(buckets.begin(), buckets.end(), [&](std::unique_ptr const& a, std::unique_ptr const& b){ return m_am.lt(a->val, b->val); }); omega_buckets.clear(); omega_buckets.reserve(buckets.size()); for (auto& b : buckets) omega_buckets.push_back(b->members); } // Helper: set I as a section if sample matches a bucket value; returns true if set bool initialize_section_from_bucket(unsigned i, std::vector>& buckets, symbolic_interval& I) { var y = i; if (!sample().is_assigned(y)) return false; anum const& y_val = sample().value(y); for (auto const& b : buckets) { if (m_am.compare(y_val, b->val) == 0) { I.section = true; auto const& ire = b->members.front(); I.l = ire.p; I.l_index = ire.i; I.u = nullptr; I.u_index = 0; return true; } } return false; } // Helper: set sector bounds from neighboring buckets; assumes buckets sorted; no-op if sample unassigned void set_sector_bounds_from_buckets(unsigned i, std::vector>& buckets, symbolic_interval& I) { var y = i; if (!sample().is_assigned(y)) return; anum const& y_val = sample().value(y); bucket_t* lower_b = nullptr; bucket_t* upper_b = nullptr; for (auto& b : buckets) { int cmp = m_am.compare(y_val, b->val); if (cmp > 0) lower_b = b.get(); else if (cmp < 0) { upper_b = b.get(); break; } } if (lower_b) { auto const& ire = lower_b->members.front(); I.l = ire.p; I.l_index = ire.i; } if (upper_b) { auto const& ire = upper_b->members.front(); I.u = ire.p; I.u_index = ire.i; } } // Step 2b: compute interval I from (sorted) buckets and current sample void compute_interval_from_buckets(unsigned i, std::vector>& buckets, symbolic_interval& I) { // default: whole line sector (-inf, +inf) I.section = false; I.l = nullptr; I.u = nullptr; I.l_index = 0; I.u_index = 0; if (initialize_section_from_bucket(i, buckets, I)) return; set_sector_bounds_from_buckets(i, buckets, I); } // Part A of construct_interval: apply pre-conditions (line 8-11 scaffolding) bool apply_property_rules(unsigned i, prop_enum prop_to_avoid) { std::vector to_refine = greatest_to_refine(i, prop_to_avoid); for (const auto& p: to_refine) apply_pre(p); return !m_fail; } // Part B of construct_interval: build (I, E, ≼) representation for level i void build_representation(unsigned i, result_struct& ret) { std::vector p_non_null; for (const auto & pr: m_Q) { if (pr.prop == prop_enum::sgn_inv_irreducible && m_pm.max_var(pr.p) == i && poly_is_not_nullified_at_sample_at_level(pr.p, i)) p_non_null.push_back(pr.p); } std::vector> buckets; std::vector roots; collect_E_and_roots(p_non_null, i, ret.E, roots); bucketize_roots(roots, buckets); build_omega_buckets(buckets, ret.omega_buckets); compute_interval_from_buckets(i, buckets, ret.I); } void remove_level_i_from_Q(std::vector & Q, unsigned i) { Q.erase(std::remove_if(Q.begin(), Q.end(), [i](const property &p) { return p.level == i; }), Q.end()); } result_struct construct_interval(unsigned i) { result_struct ret; if (!apply_property_rules(i, prop_enum::sgn_inv_irreducible)) { ret.fail = true; return ret; } build_representation(i, ret); apply_property_rules(i, prop_enum(prop_enum::holds)); ret.Q = m_Q; ret.fail = m_fail; remove_level_i_from_Q(ret.Q, i); return ret; } void apply_pre(const property& p) { TRACE(levelwise, display(tout << "p:", p) << std::endl;); NOT_IMPLEMENTED_YET(); } // return an empty vector on failure, otherwis returns the cell representations with intervals std::vector single_cell() { TRACE(levelwise, m_solver.display_assignment(tout << "sample()") << std::endl; tout << "m_P:\n"; for (const auto & p: m_P) { ::nlsat::display(tout, m_solver, polynomial_ref(p, m_pm)) << std::endl; tout << "max0_var:" << m_pm.max_var(p) << std::endl; } ); std::vector ret; m_Q = seed_properties(); // Q is the set of properties on level m_n for (unsigned i = m_n; --i > 0; ) { auto result = construct_interval(i); if (result.fail) return std::vector(); // return empty ret.push_back(result.I); m_Q = result.Q; } return ret; // the order is reversed! } }; // constructor levelwise::levelwise(nlsat::solver& solver, polynomial_ref_vector const& ps, var n, assignment const& s, pmanager& pm, anum_manager& am) : m_impl(new impl(solver, ps, n, s, pm, am)) {} levelwise::~levelwise() { delete m_impl; } std::vector levelwise::single_cell() { return m_impl->single_cell(); } } // namespace nlsat