#include "nlsat/levelwise.h" #include "nlsat/nlsat_types.h" #include "nlsat/nlsat_assignment.h" #include #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_tag; polynomial_ref poly; unsigned s_idx = -1; // index of the root function, if applicable; -1 means unspecified unsigned level = -1; // -1 means unspecified property(prop_enum pr, polynomial_ref const & pp, int si, int lvl) : prop_tag(pr), poly(pp), s_idx(si), level(lvl) {} property(prop_enum pr, polynomial_ref const & pp) : prop_tag(pr), poly(pp), s_idx(-1), level(-1) {} property(prop_enum pr, polynomial::manager& pm, unsigned lvl) : prop_tag(pr), poly(polynomial_ref(pm)), s_idx(-1), level(lvl) {} }; 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; bool m_Q_changed = false; // tracks mutations to m_Q for fixed-point iteration // 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(); } // helper overload so callers can pass either raw poly* or polynomial_ref unsigned max_var(poly* p) { return m_pm.max_var(p); } unsigned max_var(polynomial_ref const & p) { return m_pm.max_var(p); } #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 } /* Short: build the initial property set Q so the one-cell algorithm can generalize the conflict around the current sample. The main goal is to eliminate raw input polynomials whose main variable is x_{m_n} (i.e. level == m_n) by replacing them with properties. Strategy: - For factors with level < m_n: add sgn_inv(p) to Q (sign-invariance). - For factors with level == m_n: add an_del(p) and isolate their indexed roots over the sample; sort those roots and for each adjacent pair coming from distinct polynomials add ord_inv(resultant(p_j, p_{j+1})) to Q. - If any constructed polynomial (resultant, discriminant, etc.) is nullified on the sample, fail immediately. Result: Q = { sgn_inv(p) | level(p) < m_n } ∪ { an_del(p) | level(p) == m_n } ∪ { ord_inv(resultant(p_j,p_{j+1})) for adjacent roots }. */ // Helper 1: scan input polynomials, add sgn_inv / an_del properties and collect polynomials at level m_n void collect_level_properties(std::vector & Q, std::vector & ps_of_n_level) { for (unsigned i = 0; i < m_P.size(); ++i) { poly* p = m_P[i]; unsigned level = max_var(p); if (level < m_n) Q.push_back(property(prop_enum::sgn_inv_irreducible, polynomial_ref(p, m_pm), /*s_idx*/0u, /* level */ level)); else if (level == m_n){ Q.push_back(property(prop_enum::an_del, polynomial_ref(p, m_pm), /* s_idx */ -1, level)); ps_of_n_level.push_back(p); } else { SASSERT(level <= m_n); } } } // Helper 2: isolate and collect algebraic roots for the given polynomials void collect_roots_for_ps(std::vector const & ps_of_n_level, std::vector> & root_vals) { for (poly * p : ps_of_n_level) { scoped_anum_vector roots(m_am); m_am.isolate_roots(polynomial_ref(p, m_pm), undef_var_assignment(sample(), m_n), roots); unsigned num_roots = roots.size(); for (unsigned k = 0; k < num_roots; ++k) { scoped_anum v(m_am); m_am.set(v, roots[k]); root_vals.emplace_back(std::move(v), p); } } } // Helper 3: given collected roots (possibly unsorted), sort them, and add ord_inv(resultant(...)) // for adjacent roots coming from different polynomials. Avoid adding the same unordered pair twice. // Returns false on failure (e.g. when encountering an ambiguous zero resultant). bool add_adjacent_resultants(std::vector> & root_vals, std::vector & Q) { if (root_vals.size() < 2) return true; std::sort(root_vals.begin(), root_vals.end(), [&](auto const & a, auto const & b){ return m_am.lt(a.first, b.first); }); std::set> added_pairs; for (size_t j = 0; j + 1 < root_vals.size(); ++j) { poly* p1 = root_vals[j].second; poly* p2 = root_vals[j+1].second; if (p1 == p2) continue; // delineability of p1 handled by an_del unsigned id1 = polynomial::manager::id(polynomial_ref(p1, m_pm)); unsigned id2 = polynomial::manager::id(polynomial_ref(p2, m_pm)); std::pair key = id1 < id2 ? std::make_pair(id1, id2) : std::make_pair(id2, id1); if (added_pairs.find(key) != added_pairs.end()) continue; added_pairs.insert(key); polynomial_ref r(m_pm); r = resultant(polynomial_ref(p1, m_pm), polynomial_ref(p2, m_pm), m_n); if (is_const(r)) continue; if (is_zero(r)) { NOT_IMPLEMENTED_YET();// ambiguous resultant - not handled yet return false; } Q.push_back(property(prop_enum::ord_inv_irreducible, r, /*s_idx*/ -1, max_var(r))); } return true; } /* Return Q = { sgn_inv(p) | level(p) < m_n } ∪ { an_del(p) | level(p) == m_n } ∪ { ord_inv(resultant(p_j,p_{j+1})) for adjacent root functions }. */ std::vector seed_properties() { std::vector Q; std::vector ps_of_n_level; collect_level_properties(Q, ps_of_n_level); std::vector> root_vals; collect_roots_for_ps(ps_of_n_level, root_vals); if (!add_adjacent_resultants(root_vals, Q)) { m_fail = true; return Q; } 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.poly == b.poly && dominates(a.prop_tag, b.prop_tag); } 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_tag); if (pr.level != -1) out << ", level:" << pr.level; if (pr.s_idx != -1) out << ", s_idx:" << pr.s_idx; if (pr.poly) { out << ", poly:"; ::nlsat::display(out, m_solver, pr.poly); } 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_tag != 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; } } } auto poly_id = [cand](unsigned i) { return cand[i].poly == nullptr? UINT_MAX: polynomial::manager::id(cand[i].poly);}; // 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{ poly_id(i), static_cast(cand[i].prop_tag), 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; } // 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, result_struct* rs) { // Iterate until no mutation to m_Q occurs (fixed-point). We avoid copying m_Q // by using a change flag that is set by mutating helpers (add_to_Q_if_new / erase_from_Q). if (m_fail) return false; do { m_Q_changed = false; std::vector to_refine = greatest_to_refine(i, prop_to_avoid); for (const auto& p : to_refine) { apply_pre(p, rs); if (m_fail) return false; } } while (m_Q_changed && !m_fail); 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_tag == prop_enum::sgn_inv_irreducible && max_var(pr.poly) == i && !coeffs_are_zeroes_on_sample(pr.poly, m_pm, sample(), m_am )) p_non_null.push_back(pr.poly.get()); } 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); } // Helper: add a property to m_Q if an equivalent one is not already present. // Equivalence: same prop_tag and same level; if pr.poly is non-null, require the same poly as well. void add_to_Q_if_new(const property & pr) { for (auto const & q : m_Q) { if (q.prop_tag != pr.prop_tag) continue; if (q.level != pr.level) continue; if (pr.poly) { if (q.poly == pr.poly) return; else continue; } // pr.poly is null -> match by prop_tag + level only return; } m_Q.push_back(pr); m_Q_changed = true; } 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()); } void erase_from_Q(const property& p) { auto it = std::find_if(m_Q.begin(), m_Q.end(), [&](const property & q) { return q.prop_tag == p.prop_tag && q.poly == p.poly && q.level == p.level && q.s_idx == p.s_idx; }); SASSERT(it != m_Q.end()); m_Q.erase(it); m_Q_changed = true; } result_struct construct_interval(unsigned i) { result_struct ret; if (!apply_property_rules(i, prop_enum::sgn_inv_irreducible, nullptr)) { ret.fail = true; return ret; } build_representation(i, ret); apply_property_rules(i, prop_enum(prop_enum::holds), & ret); // (moved Rule 1.1 precondition handling into apply_pre_connected) ret.Q = m_Q; ret.fail = m_fail; remove_level_i_from_Q(ret.Q, i); return ret; } // Extracted helper: handle ord_inv(discriminant_{x_{i+1}}(p)) for an_del pre-processing void add_ord_inv_discriminant_for(const property& p) { polynomial_ref disc(m_pm); disc = discriminant(p.poly, p.level); TRACE(levelwise, ::nlsat::display(tout << "discriminant: ", m_solver, disc) << "\n";); if (!is_const(disc)) { if (coeffs_are_zeroes_on_sample(disc, m_pm, sample(), m_am)) { m_fail = true; // ambiguous multiplicity -- not handled yet NOT_IMPLEMENTED_YET(); return; } else { unsigned lvl = max_var(disc); add_to_Q_if_new(property(prop_enum::ord_inv_irreducible, disc, /*s_idx*/ 0u, lvl)); } } } // Extracted helper: handle sgn_inv(leading_coefficient_{x_{i+1}}(p)) for an_del pre-processing void add_sgn_inv_leading_coeff_for(const property& p) { poly * pp = p.poly.get(); unsigned deg = m_pm.degree(pp, p.level); if (deg > 0) { polynomial_ref lc(m_pm); lc = m_pm.coeff(pp, p.level, deg); if (!is_const(lc)) { if (coeffs_are_zeroes_on_sample(lc, m_pm, sample(), m_am)) { NOT_IMPLEMENTED_YET(); // leading coeff vanished as polynomial -- not handled yet } else { unsigned lvl = max_var(lc); add_to_Q_if_new(property(prop_enum::sgn_inv_irreducible, lc, /*s_idx*/ 0u, lvl)); } } } } // Extracted helper: check preconditions for an_del property; returns true if ok, false otherwise. bool precondition_on_an_del(const property& p) { if (!p.poly) { TRACE(levelwise, tout << "apply_pre: an_del with null poly -> fail" << std::endl;); m_fail = true; return false; } if (p.level == static_cast(-1)) { TRACE(levelwise, tout << "apply_pre: an_del with unspecified level -> skip" << std::endl;); NOT_IMPLEMENTED_YET(); return false; } // If p is nullified on the sample for its level we must abort (Rule 4.1) if (coeffs_are_zeroes_on_sample(p.poly, m_pm, sample(), m_am)) { TRACE(levelwise, tout << "Rule 4.1: polynomial nullified at sample -> failing" << std::endl;); m_fail = true; NOT_IMPLEMENTED_YET(); return false; } return true; } void apply_pre_an_del(const property& p) { if (!precondition_on_an_del(p)) return; // Pre-conditions for an_del(p) per Rule 4.1 unsigned lvl = (p.level > 0) ? p.level - 1 : 0; add_to_Q_if_new(property(prop_enum::an_sub, m_pm, lvl)); add_to_Q_if_new(property(prop_enum::connected, m_pm, lvl)); add_to_Q_if_new(property(prop_enum::non_null, p.poly, p.s_idx, p.level)); add_ord_inv_discriminant_for(p); if (m_fail) return; add_sgn_inv_leading_coeff_for(p); erase_from_Q(p); } // Pre-processing for connected(i) (Rule 4.11) void apply_pre_connected(const property & p, result_struct* rs) { TRACE(levelwise, tout << "connected:"; if (p.level != static_cast(-1)) tout << " level=" << p.level; tout << std::endl;); SASSERT(p.level != static_cast(-1)); // Rule 4.11 special-case: if the connected property refers to level 0 there's nothing to refine // further; just remove the property from Q and return. if (p.level == 0) { TRACE(levelwise, tout << "apply_pre_connected: level 0 -> erasing connected property and returning" << std::endl;); erase_from_Q(p); return; } // p.level > 0 if (!rs) return; // no change since the interval etc is not there // Rule 1.1 precondition: when processing connected(i) we must ensure the next lower level // has connected(i-1) and repr(I,s) available. Add those markers to m_Q so they propagate. add_to_Q_if_new(property(prop_enum::connected, m_pm, /*level*/ p.level - 1)); add_to_Q_if_new(property(prop_enum::repr, m_pm, /*level*/ p.level - 1)); NOT_IMPLEMENTED_YET(); // todo!!!! add missing preconditions // connected property has been processed erase_from_Q(p); } void apply_pre_non_null(const property& p) { TRACE(levelwise, tout << "apply_pre_non_null:"; if (p.level != static_cast(-1)) tout << " level=" << p.level; tout << std::endl;); // First try subrule 1 of Rule 4.2. If it succeeds we do not apply the fallback (subrule 2). if (try_non_null_via_coeffs(p, nullptr)) return; // fallback: apply the first subrule apply_pre_non_null_fallback(p); } bool have_non_zero_const(const polynomial_ref& p, unsigned level) { unsigned deg = m_pm.degree(p, level); for (unsigned j = deg; --j > 0; ) if (m_pm.nonzero_const_coeff(p.get(), level, j)) return true; return false; } // Helper for Rule 4.2, subrule 2: // If some coefficient c_j of p is constant non-zero at the sample, or // if c_j evaluates non-zero at the sample and we already have sgn_inv(c_j) in m_Q, // then non_null(p) holds on the region represented by 'rs' (if provided). // Returns true if non_null was established and the property p was removed. bool try_non_null_via_coeffs(const property& p, result_struct* rs) { if (have_non_zero_const(p.poly, p.level)) { TRACE(levelwise, tout << "have a non-zero const coefficient\n";); erase_from_Q(p); return true; } poly* pp = p.poly.get(); unsigned deg = m_pm.degree(pp, p.level); for (unsigned j = 0; j <= deg; ++j) { polynomial_ref coeff(m_pm); coeff = m_pm.coeff(pp, p.level, j); // If coefficient is constant and non-zero at sample -> non_null holds if (is_const(coeff)) { SASSERT(m_pm.nonzero_const_coeff(pp, p.level, j)); continue; } if (sign(coeff, sample(), m_am) == 0) continue; auto it = std::find_if(m_Q.begin(), m_Q.end(), [&](const property & q) { return (q.prop_tag == prop_enum::sgn_inv_irreducible || q.prop_tag == prop_enum::sgn_inv_reducible) && q.poly == p.poly; }); if (it != m_Q.end()) { erase_from_Q(p); return true; } add_to_Q_if_new(property(prop_enum::sgn_inv_reducible, coeff, 0u, max_var(coeff))); erase_from_Q(p); return true; } return false; } // Helper for Rule 4.2, subrule 1: fallback when subrule 2 does not apply. // sample(s)(R), degx_{i+1} (p) > 1, disc(x_{i+1} (p)(s)) ̸= 0, sgn_inv(disc(x_{i+1} (p))(R) void apply_pre_non_null_fallback(const property& p) { // basic sanity checks if (!p.poly) { TRACE(levelwise, tout << "apply_pre_non_null_fallback: null poly -> fail" << std::endl;); m_fail = true; return; } if (p.level == static_cast(-1)) { TRACE(levelwise, tout << "apply_pre_non_null_fallback: unspecified level -> skip" << std::endl;); return; } poly * pp = p.poly.get(); unsigned deg = m_pm.degree(pp, p.level); // fallback applies only for degree > 1 if (deg <= 1) return; // compute discriminant w.r.t. the variable at p.level polynomial_ref disc(m_pm); disc = discriminant(p.poly, p.level); TRACE(levelwise, ::nlsat::display(tout << "discriminant: ", m_solver, disc) << "\n";); // If discriminant evaluates to zero at the sample, we cannot proceed // (ambiguous multiplicity) -> fail per instruction if (sign(disc, sample(), m_am) == 0) { TRACE(levelwise, tout << "apply_pre_non_null_fallback: discriminant vanishes at sample -> failing" << std::endl;); m_fail = true; NOT_IMPLEMENTED_YET(); return; } // If discriminant is non-constant, add sign-invariance requirement for it if (!is_const(disc)) { unsigned lvl = max_var(disc); add_to_Q_if_new(property(prop_enum::sgn_inv_irreducible, disc, /*s_idx*/ 0u, lvl)); } // non_null is established by the discriminant being non-zero at the sample erase_from_Q(p); } void apply_pre(const property& p, result_struct* rs) { TRACE(levelwise, display(tout << "property p:", p) << std::endl;); if (p.prop_tag == prop_enum::an_del) apply_pre_an_del(p); else if (p.prop_tag == prop_enum::connected) apply_pre_connected(p, rs ); else if (p.prop_tag == prop_enum::non_null) apply_pre_non_null(p); else NOT_IMPLEMENTED_YET(); } // return an empty vector on failure, otherwise 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 << "max_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 apply_property_rules(m_n, prop_enum::_count, nullptr); // reduce the level by one to be consumed by construct_interval 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