From cf3552c0296417f45651979103855a291e0a0d62 Mon Sep 17 00:00:00 2001 From: Lev Nachmanson Date: Mon, 23 Feb 2026 09:59:08 -1000 Subject: [PATCH 1/3] suppress witness subs optimization Signed-off-by: Lev Nachmanson --- src/nlsat/levelwise.cpp | 14 +++++++++----- src/nlsat/nlsat_params.pyg | 2 ++ src/nlsat/nlsat_solver.cpp | 6 ++++++ src/nlsat/nlsat_solver.h | 2 ++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/nlsat/levelwise.cpp b/src/nlsat/levelwise.cpp index 58129e0b9..9401226d5 100644 --- a/src/nlsat/levelwise.cpp +++ b/src/nlsat/levelwise.cpp @@ -55,6 +55,8 @@ namespace nlsat { unsigned m_level = 0; // current level being processed unsigned m_spanning_tree_threshold = 3; // minimum both-side count for spanning tree + bool m_no_sign_lc = false; + bool m_no_sign_disc = false; unsigned m_l_rf = UINT_MAX; // position of lower bound in m_rel.m_rfunc unsigned m_u_rf = UINT_MAX; // position of upper bound in m_rel.m_rfunc, UINT_MAX in section case @@ -260,6 +262,8 @@ namespace nlsat { m_I.emplace_back(m_pm); m_spanning_tree_threshold = m_solver.lws_spt_threshold(); + m_no_sign_lc = m_solver.lws_no_sign_lc(); + m_no_sign_disc = m_solver.lws_no_sign_disc(); } // Handle a polynomial whose every coefficient evaluates to zero at the sample. @@ -443,7 +447,7 @@ namespace nlsat { lc = m_pm.coeff(p, x, deg); TRACE(lws, m_pm.display(tout << " adding lc: ", lc) << "\n";); request_factorized(lc); - if (add_nzero_coeff && lc && sign(lc)) + if (add_nzero_coeff && !m_no_sign_lc && lc && sign(lc)) add_nzero_coeff = false; } @@ -455,7 +459,7 @@ namespace nlsat { // If p is nullified at some point then at this point discriminant well be evaluated // to zero, as can be seen from the Sylvester matrix which would // have at least one zero row. - if (add_nzero_coeff && sign(disc)) // we can avoid adding a nonzero_coeff if sign(disc) != 0 + if (add_nzero_coeff && !m_no_sign_disc && sign(disc)) // we can avoid adding a nonzero_coeff if sign(disc) != 0 add_nzero_coeff = false; } } @@ -1165,7 +1169,7 @@ namespace nlsat { // No need for an additional coefficient witness in this case. polynomial_ref witness = m_witnesses[i]; if (add_lc && witness && !is_const(witness)) - if (lc && !is_zero(lc) && sign(lc)) + if (lc && !is_zero(lc) && !m_no_sign_lc && sign(lc)) witness = polynomial_ref(m_pm); add_projection_for_poly(p, m_level, witness, add_lc, add_disc); @@ -1309,14 +1313,14 @@ namespace nlsat { bool add_lc = true; if (!poly_has_roots(i)) - if (lc && !is_zero(lc) && sign(lc)) + if (lc && !is_zero(lc) && !m_no_sign_lc && sign(lc)) add_lc = false; // if the leading coefficient is already non-zero at the sample // AND we're adding lc, we do not need to project an additional non-null coefficient witness. polynomial_ref witness = m_witnesses[i]; if (add_lc && witness && !is_const(witness)) - if (lc && !is_zero(lc) && sign(lc)) + if (lc && !is_zero(lc) && !m_no_sign_lc && sign(lc)) witness = polynomial_ref(m_pm); // zero the witnsee as lc will be the witness add_projection_for_poly(p, m_n, witness, add_lc, true); //true to add the discriminant } diff --git a/src/nlsat/nlsat_params.pyg b/src/nlsat/nlsat_params.pyg index d63a96e91..dbb97ea4f 100644 --- a/src/nlsat/nlsat_params.pyg +++ b/src/nlsat/nlsat_params.pyg @@ -24,5 +24,7 @@ def_module_params('nlsat', ('known_sat_assignment_file_name', STRING, "", "the file name of a known solution: used for debugging only"), ('lws', BOOL, True, "apply levelwise."), ('lws_spt_threshold', UINT, 4, "minimum both-side polynomial count to apply spanning tree optimization; < 2 disables spanning tree"), + ('lws_no_sign_lc', BOOL, False, "suppress sign() call on leading coefficient in add_projection_for_poly"), + ('lws_no_sign_disc', BOOL, False, "suppress sign() call on discriminant in add_projection_for_poly"), ('canonicalize', BOOL, True, "canonicalize polynomials.") )) diff --git a/src/nlsat/nlsat_solver.cpp b/src/nlsat/nlsat_solver.cpp index fef3bcccf..6376ee12c 100644 --- a/src/nlsat/nlsat_solver.cpp +++ b/src/nlsat/nlsat_solver.cpp @@ -251,6 +251,8 @@ namespace nlsat { bool m_apply_lws; bool m_last_conflict_used_lws = false; // Track if last conflict explanation used levelwise unsigned m_lws_spt_threshold = 3; + bool m_lws_no_sign_lc = false; + bool m_lws_no_sign_disc = false; imp(solver& s, ctx& c): m_ctx(c), m_solver(s), @@ -312,6 +314,8 @@ namespace nlsat { m_debug_known_solution_file_name = p.known_sat_assignment_file_name(); m_apply_lws = p.lws(); m_lws_spt_threshold = p.lws_spt_threshold(); // 0 disables spanning tree + m_lws_no_sign_lc = p.lws_no_sign_lc(); + m_lws_no_sign_disc = p.lws_no_sign_disc(); m_check_lemmas |= !(m_debug_known_solution_file_name.empty()); m_ism.set_seed(m_random_seed); @@ -4718,4 +4722,6 @@ namespace nlsat { } bool solver::apply_levelwise() const { return m_imp->m_apply_lws; } unsigned solver::lws_spt_threshold() const { return m_imp->m_lws_spt_threshold; } + bool solver::lws_no_sign_lc() const { return m_imp->m_lws_no_sign_lc; } + bool solver::lws_no_sign_disc() const { return m_imp->m_lws_no_sign_disc; } }; diff --git a/src/nlsat/nlsat_solver.h b/src/nlsat/nlsat_solver.h index 566a7c09e..118124c6f 100644 --- a/src/nlsat/nlsat_solver.h +++ b/src/nlsat/nlsat_solver.h @@ -249,6 +249,8 @@ namespace nlsat { assignment& sample(); bool apply_levelwise() const; unsigned lws_spt_threshold() const; + bool lws_no_sign_lc() const; + bool lws_no_sign_disc() const; void reset(); void collect_statistics(statistics & st); void reset_statistics(); From 0074de0fcee7c7407389e1f702851e82c62282a1 Mon Sep 17 00:00:00 2001 From: Lev Nachmanson Date: Tue, 24 Feb 2026 09:19:13 -1000 Subject: [PATCH 2/3] improve non-zero witness substitution logic Signed-off-by: Lev Nachmanson --- src/nlsat/levelwise.cpp | 38 ++++++++++++++++++++------------------ src/nlsat/nlsat_params.pyg | 4 ++-- src/nlsat/nlsat_solver.cpp | 14 +++++++------- src/nlsat/nlsat_solver.h | 4 ++-- 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/nlsat/levelwise.cpp b/src/nlsat/levelwise.cpp index 9401226d5..fdfb80fb0 100644 --- a/src/nlsat/levelwise.cpp +++ b/src/nlsat/levelwise.cpp @@ -55,8 +55,8 @@ namespace nlsat { unsigned m_level = 0; // current level being processed unsigned m_spanning_tree_threshold = 3; // minimum both-side count for spanning tree - bool m_no_sign_lc = false; - bool m_no_sign_disc = false; + bool m_witness_subs_lc = true; + bool m_witness_subs_disc = false; unsigned m_l_rf = UINT_MAX; // position of lower bound in m_rel.m_rfunc unsigned m_u_rf = UINT_MAX; // position of upper bound in m_rel.m_rfunc, UINT_MAX in section case @@ -262,8 +262,8 @@ namespace nlsat { m_I.emplace_back(m_pm); m_spanning_tree_threshold = m_solver.lws_spt_threshold(); - m_no_sign_lc = m_solver.lws_no_sign_lc(); - m_no_sign_disc = m_solver.lws_no_sign_disc(); + m_witness_subs_lc = m_solver.lws_witness_subs_lc(); + m_witness_subs_disc = m_solver.lws_witness_subs_disc(); } // Handle a polynomial whose every coefficient evaluates to zero at the sample. @@ -447,7 +447,7 @@ namespace nlsat { lc = m_pm.coeff(p, x, deg); TRACE(lws, m_pm.display(tout << " adding lc: ", lc) << "\n";); request_factorized(lc); - if (add_nzero_coeff && !m_no_sign_lc && lc && sign(lc)) + if (add_nzero_coeff && m_witness_subs_disc && lc && sign(lc)) add_nzero_coeff = false; } @@ -459,7 +459,7 @@ namespace nlsat { // If p is nullified at some point then at this point discriminant well be evaluated // to zero, as can be seen from the Sylvester matrix which would // have at least one zero row. - if (add_nzero_coeff && !m_no_sign_disc && sign(disc)) // we can avoid adding a nonzero_coeff if sign(disc) != 0 + if (add_nzero_coeff && m_witness_subs_disc && sign(disc)) // we can avoid adding a nonzero_coeff if sign(disc) != 0 add_nzero_coeff = false; } } @@ -1169,7 +1169,7 @@ namespace nlsat { // No need for an additional coefficient witness in this case. polynomial_ref witness = m_witnesses[i]; if (add_lc && witness && !is_const(witness)) - if (lc && !is_zero(lc) && !m_no_sign_lc && sign(lc)) + if (lc && !is_zero(lc) && m_witness_subs_lc && sign(lc)) witness = polynomial_ref(m_pm); add_projection_for_poly(p, m_level, witness, add_lc, add_disc); @@ -1304,25 +1304,27 @@ namespace nlsat { collect_non_null_witnesses(); add_adjacent_root_resultants(); - // Projections (coeff witness, disc, leading coeff). + // Projection: witness, disc, lc for (unsigned i = 0; i < m_level_ps.size(); ++i) { polynomial_ref p(m_level_ps.get(i), m_pm); polynomial_ref lc(m_pm); unsigned deg = m_pm.degree(p, m_n); lc = m_pm.coeff(p, m_n, deg); + // Projective delineability optimization, Lemma 3.2 of "Projective Delineability + // for Single Cell Construction": if p is projectively delineable on R, + // ldcf(p)(s) != 0, and p has no real roots at s, then p is delineable on R + // without requiring sign-invariance of the leading coefficient. + // Projective delineability is ensured by adding the discriminant, Theorem 3.1, + // and non-nullification is ensured by the witness. bool add_lc = true; - if (!poly_has_roots(i)) - if (lc && !is_zero(lc) && !m_no_sign_lc && sign(lc)) - add_lc = false; + if (!poly_has_roots(i) && lc && !is_zero(lc) && sign(lc)) { + add_lc = false; + polynomial_ref null_ref(m_pm); + m_witnesses[i] = null_ref; + } - // if the leading coefficient is already non-zero at the sample - // AND we're adding lc, we do not need to project an additional non-null coefficient witness. - polynomial_ref witness = m_witnesses[i]; - if (add_lc && witness && !is_const(witness)) - if (lc && !is_zero(lc) && !m_no_sign_lc && sign(lc)) - witness = polynomial_ref(m_pm); // zero the witnsee as lc will be the witness - add_projection_for_poly(p, m_n, witness, add_lc, true); //true to add the discriminant + add_projection_for_poly(p, m_n, m_witnesses[i], add_lc, true); //true to add the discriminant } } diff --git a/src/nlsat/nlsat_params.pyg b/src/nlsat/nlsat_params.pyg index dbb97ea4f..b0e0c884f 100644 --- a/src/nlsat/nlsat_params.pyg +++ b/src/nlsat/nlsat_params.pyg @@ -24,7 +24,7 @@ def_module_params('nlsat', ('known_sat_assignment_file_name', STRING, "", "the file name of a known solution: used for debugging only"), ('lws', BOOL, True, "apply levelwise."), ('lws_spt_threshold', UINT, 4, "minimum both-side polynomial count to apply spanning tree optimization; < 2 disables spanning tree"), - ('lws_no_sign_lc', BOOL, False, "suppress sign() call on leading coefficient in add_projection_for_poly"), - ('lws_no_sign_disc', BOOL, False, "suppress sign() call on discriminant in add_projection_for_poly"), + ('lws_witness_subs_lc', BOOL, True, "try substitute the non-nullified witness by the lc"), + ('lws_witness_subs_disc', BOOL, False, "try substitute the non-nullified witness by the discriminant"), ('canonicalize', BOOL, True, "canonicalize polynomials.") )) diff --git a/src/nlsat/nlsat_solver.cpp b/src/nlsat/nlsat_solver.cpp index 6376ee12c..e5e104a33 100644 --- a/src/nlsat/nlsat_solver.cpp +++ b/src/nlsat/nlsat_solver.cpp @@ -250,9 +250,9 @@ namespace nlsat { std::string m_debug_known_solution_file_name; bool m_apply_lws; bool m_last_conflict_used_lws = false; // Track if last conflict explanation used levelwise - unsigned m_lws_spt_threshold = 3; - bool m_lws_no_sign_lc = false; - bool m_lws_no_sign_disc = false; + unsigned m_lws_spt_threshold = 3; + bool m_lws_witness_subs_lc = true; + bool m_lws_witness_subs_disc = false; imp(solver& s, ctx& c): m_ctx(c), m_solver(s), @@ -314,8 +314,8 @@ namespace nlsat { m_debug_known_solution_file_name = p.known_sat_assignment_file_name(); m_apply_lws = p.lws(); m_lws_spt_threshold = p.lws_spt_threshold(); // 0 disables spanning tree - m_lws_no_sign_lc = p.lws_no_sign_lc(); - m_lws_no_sign_disc = p.lws_no_sign_disc(); + m_lws_witness_subs_lc = p. lws_witness_subs_lc(); + m_lws_witness_subs_disc = p.lws_witness_subs_disc(); m_check_lemmas |= !(m_debug_known_solution_file_name.empty()); m_ism.set_seed(m_random_seed); @@ -4722,6 +4722,6 @@ namespace nlsat { } bool solver::apply_levelwise() const { return m_imp->m_apply_lws; } unsigned solver::lws_spt_threshold() const { return m_imp->m_lws_spt_threshold; } - bool solver::lws_no_sign_lc() const { return m_imp->m_lws_no_sign_lc; } - bool solver::lws_no_sign_disc() const { return m_imp->m_lws_no_sign_disc; } + bool solver::lws_witness_subs_lc() const { return m_imp->m_lws_witness_subs_lc; } + bool solver::lws_witness_subs_disc() const { return m_imp->m_lws_witness_subs_disc; } }; diff --git a/src/nlsat/nlsat_solver.h b/src/nlsat/nlsat_solver.h index 118124c6f..ff970fefa 100644 --- a/src/nlsat/nlsat_solver.h +++ b/src/nlsat/nlsat_solver.h @@ -249,8 +249,8 @@ namespace nlsat { assignment& sample(); bool apply_levelwise() const; unsigned lws_spt_threshold() const; - bool lws_no_sign_lc() const; - bool lws_no_sign_disc() const; + bool lws_witness_subs_lc() const; + bool lws_witness_subs_disc() const; void reset(); void collect_statistics(statistics & st); void reset_statistics(); From 0835420cc12cc56263efadd1696ba56b0a7884ef Mon Sep 17 00:00:00 2001 From: Lev Nachmanson Date: Tue, 24 Feb 2026 15:24:35 -1000 Subject: [PATCH 3/3] change the default of param lws_subs_witness_disc to true Signed-off-by: Lev Nachmanson --- src/nlsat/nlsat_params.pyg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nlsat/nlsat_params.pyg b/src/nlsat/nlsat_params.pyg index b0e0c884f..048fcf521 100644 --- a/src/nlsat/nlsat_params.pyg +++ b/src/nlsat/nlsat_params.pyg @@ -25,6 +25,6 @@ def_module_params('nlsat', ('lws', BOOL, True, "apply levelwise."), ('lws_spt_threshold', UINT, 4, "minimum both-side polynomial count to apply spanning tree optimization; < 2 disables spanning tree"), ('lws_witness_subs_lc', BOOL, True, "try substitute the non-nullified witness by the lc"), - ('lws_witness_subs_disc', BOOL, False, "try substitute the non-nullified witness by the discriminant"), + ('lws_witness_subs_disc', BOOL, True, "try substitute the non-nullified witness by the discriminant"), ('canonicalize', BOOL, True, "canonicalize polynomials.") ))