3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-02-06 09:16:18 +00:00
z3/src/nlsat/levelwise.cpp
Lev Nachmanson 63f05ff6e6
Merge with branch lws (#8498)
* t0

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t1

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t2

* scaffoldin

* scaffolding

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* closer to the paper

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* more scaffolding

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* define symbolic_interval

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* use std::map instead of std::unordered_map

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* more accurate init of the relation between polynomial properties

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* pass anum_manager to levelwise, crash on sign

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* pass pmanager

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* create free function display functions

* use new display functions

* pass nlsat::solver to levelwise

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* add trace tag for levelwise

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* refactor

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* refactor

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* define indexed root expression

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* refact lws

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* refact lws

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* refactor lws

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* trying to figure out right indices

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* rename explain::main_operator to compute_conflict_explanation

* preprocess the input of levelwise to drop a level

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* ttt

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* renaming

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* rename

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* work on seed_properties

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* work on seed_properties

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* work on seed_properties

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* move a comment

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* simplify

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* simplify

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* debug

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* refactor and assert _irreducible

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* add a display method

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* simplify

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* simplify

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* remove erase_from_Q

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* ignore holds properties

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* got a section

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* introdure mk_prop

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* remove a parameter

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* add parameter to suppress/enable levelwise

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* comment

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fixing factoring and hitting NOT_IMPLEMENTED on ir_ord

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* adding ir_ord

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* produce more literals but creating sat lemmas

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* try iterative factoring

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* new file

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* create irreducible polynomials on init

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* add a guard on m_fail

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* process level 0 as well

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* remove a warning

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* debug

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* prepare to fill the relation

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* filling the relation

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* separate the lower and upper bound root functions

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fix an assert statement

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* create a better queue on properties

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* normalize before pushing

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* relax an assert

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* rebase with master

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* add stats to track levelwise calls

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* catch and fail on an exception

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fix a bug in Rule 4.2

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* remove debug instruction

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* call levelwise on a correct set of polynomials

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* cosmetics

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* use polynomial_ref instead of poly*

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* do not refactor again multivariate polynomials

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* canonicalize polinomals in todo_set

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* canonicalize polynomials in nlsat

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

* normalize polynomials

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* try not to fail in add_sgn_inv_leading_coeff_for

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* use the cache consistently

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* unsound state

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* unsound state

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* handle the zero case in add_ord_inv_resultant

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* optimizations by using cached psc

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* make normalize optional

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* Revert "make normalize optional"

This reverts commit c80cfb0b8e.

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* cleanup and more caching

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* better sort of root functions

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* index bug

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* with resultant calculation ignore one of p and q with a common root

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fix the duplicate bug

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

* simplify by removing back propagation

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* hook up different relation build strategies for lws

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* introduce isolate_root_closest

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fix a bug with non-adding ldcf

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* simple choice of non-vanishing

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* restore choose_non_zero_coeff

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* efficient sort of root functions

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* avoid ldcf with the projective theorem

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* omit some disc

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* use std_vector more

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* avoid a compare call

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* try optimizing build_interval_and_relation

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* discard a discriminant only in the section case

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* refactor

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* refactor

* refactor

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* cache the polynomial roots

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* Revert "cache the polynomial roots"

This reverts commit aefcd16aaa.

* ignore const non-null witnesses

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* toward more like SMT-RAT split

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* align with SMT-RAT

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* disables some heuristics in section

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* Implement chain noLdcf optimization matching SMT-RAT

Add find_partition_boundary() helper to locate the boundary between
lower and upper root partitions in m_rfunc.

Implement compute_omit_lc_sector_chain() and compute_omit_lc_section_chain()
following SMT-RAT's OneCellCAD.h logic:
- Omit ldcf for extreme of lower chain (index 0) if it appears on upper side
- Omit ldcf for extreme of upper chain (last index) if it appears on lower side

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Restrict noDisc optimization to section_lowest_degree only

Match SMT-RAT behavior: noDisc (discriminant omission for leaves
connected only to section polynomial) is only applied for
sectionHeuristic == 3 (lowest_degree), not for biggest_cell or chain.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Cache partition boundary to avoid repeated algebraic number comparisons

Store the partition boundary (index of first root > sample) in
relation_E after sorting root functions. Use this cached value
in compute_omit_lc_sector_chain() and compute_omit_lc_section_chain()
instead of recomputing via algebraic number comparisons.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Refactor levelwise: consolidate partition indices into m_l_rf/m_u_rf

Replace scattered local l_index/u_index parameters and m_partition_boundary
with two impl members:
- m_l_rf: position of lower bound in m_rel.m_rfunc
- m_u_rf: position of upper bound in m_rel.m_rfunc (UINT_MAX in section case)

This simplifies the code by:
- Removing parameter passing through multiple function calls
- Removing redundant m_partition_boundary from relation_E
- Making the partition state explicit in impl

Also clean up nlsat_explain.cpp to trust root_function_interval invariants:
- Section case: assert l and l_index are valid instead of defensive check
- Sector bounds: !l_inf()/!u_inf() implies valid polynomial and index

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Refactor levelwise: use member variables for per-level state

Replace local variables and function parameters with member variables:
- m_level_ps: polynomials at current level (owned)
- m_level_tags: tags for each polynomial (owned)
- m_witnesses: non-zero coefficient witnesses
- m_poly_has_roots: which polynomials have roots
- m_todo: pointer to todo_set

Functions now use these member variables directly:
- extract_max_tagged() fills m_level_ps/m_level_tags and sets m_level
- process_level() and process_top_level() are now parameterless
- All helper functions use member variables instead of parameters

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Refactor levelwise: change m_todo from pointer to member

- Change m_todo from todo_set* to todo_set
- Initialize m_todo in constructor initializer list
- Use m_todo.reset() in single_cell_work instead of creating local todo_set
- Replace pointer access (m_todo->) with member access (m_todo.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add dynamic heuristic selection for levelwise projection

Implement weight-based dynamic selection of projection heuristics in
levelwise CAD. The weight function w(p, level) = deg(p, level) estimates
projection complexity, with w(res(a,b)) ≈ w(a) + w(b).

At each level, all three heuristics (biggest_cell, chain, lowest_degree)
are evaluated and the one with minimum estimated resultant weight is
selected. When fewer than 2 root functions exist, the default heuristic
is used since all produce equivalent results.

Add parameter nlsat.lws_dynamic_heuristic (default: true) to enable or
disable dynamic selection. When disabled, the static heuristic from
lws_sector_rel_mode/lws_section_rel_mode is used.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* local optimization

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* call omit_lc only when both bounds are present

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* use std_vector

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* remove m_level_tags

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* count added lcs in the heuriistic estimates

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* add both side spanning tree heuristic

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* Fix nlsat projection bug: ensure polynomials with assumptions are also projected

When polynomials are added as assumptions (via add_assumption or ensure_sign),
they must also be added to the projection set (m_todo) to ensure proper cell
construction. Previously, assumptions were added without corresponding projection,
leading to unsound lemmas.

Fixes:
1. In normalize(): collect lower-stage polynomials in m_lower_stage_polys and
   add them to m_ps in main() before projection.
2. In ensure_sign(): call insert_fresh_factors_in_todo(p) after adding assumption.
3. In project_cdcac(): when levelwise fails, use flet to set m_add_all_coeffs=true
   for the fallback projection.

* Remove deprecated project_original and cell_sample parameter

- Remove project_original() function from nlsat_explain.cpp
- Remove m_sample_cell_project member variable
- Simplify project() to just call project_cdcac()
- Remove cell_sample parameter from nlsat_params.pyg
- Update nlsat_solver.cpp to remove cell_sample() references
- Update nlsat_explain.h constructor signature

* Enforce bound polynomial LC protection in compute_omit_lc functions

Move the invariant that bound-defining polynomials must never have their
LC omitted from add_level_projections_sector() into the source functions:
- compute_omit_lc_both_sides()
- compute_omit_lc_chain_extremes()

This centralizes the protection and removes the redundant override check.

* fix the build

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* bug fixes

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* restore a deleted function

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* remove sector/section stats

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* Simplify levelwise: remove chain/lowest_degree heuristics, unify relation
   mode

     - Remove chain and lowest_degree heuristics, keep only biggest_cell and spanning_tree
     - Unify m_sector_relation_mode and m_section_relation_mode into single m_rel_mode
     - Remove lws_rel_mode, lws_sector_rel_mode, lws_section_rel_mode, lws_dynamic_heuristic params
     - lws_spt_threshold < 2 now disables spanning tree (single tuning parameter)
     - Restore noDisc optimization for spanning_tree leaves connected to boundary
     - Add noDisc for sector with same_boundary_poly (treat like section case)
     - Significant code reduction (~390 lines removed)

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fix bug with skipping too many discriminants

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* t

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* simplifications and bug fixes in lws, use static_tree only with sector + different bound polynomials, otherwise us biggest cell

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* bug fixes and cleanup

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* add the discriminant in degenerated case

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fix a bug with skipping a vanishing discriminant

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* remove the unsound optimization

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fiddle with heuristics

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* preserve random seed in nlsat_solver::check_lemma

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fix a typo in poly_has_roots

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* add lc(p) and disc(p) for a rootless p in section case

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* remove warnings

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* untracking .beads

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fix the explosion in m_todo with lws.false

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* fix issue 8397

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* set default to nlsat.lws=false for the merge with master

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

* set nlsat.lws=true by default, enable levelwise

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>

---------

Signed-off-by: Lev Nachmanson <levnach@hotmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 09:52:02 -08:00

1428 lines
63 KiB
C++

#include "nlsat/levelwise.h"
#include "nlsat/nlsat_types.h"
#include "nlsat/nlsat_assignment.h"
#include "math/polynomial/algebraic_numbers.h"
#include "math/polynomial/polynomial.h"
#include "nlsat_common.h"
#include "util/z3_exception.h"
#include "util/vector.h"
#include "util/trace.h"
#include <algorithm>
#include <cstdint>
#include <set>
#include <unordered_map>
#include <utility>
#include <vector>
static bool is_set(unsigned k) { return static_cast<int>(k) != -1; }
// Helper for sparse vector access with default value
template<typename T>
static T vec_get(std_vector<T> const& v, unsigned idx, T def) {
return idx < v.size() ? v[idx] : def;
}
// Helper for sparse vector write with auto-resize
template<typename T>
static void vec_setx(std_vector<T>& v, unsigned idx, T val, T def) {
if (idx >= v.size())
v.resize(idx + 1, def);
v[idx] = val;
}
namespace nlsat {
// The three projection modes for a level:
// 1. section_biggest_cell: Sample is on a root. All disc/lc added.
// 2. sector_biggest_cell: Sample between roots. noLdcf optimization only.
// 3. sector_spanning_tree: Sample between roots with many both-side polys.
// Both noLdcf and noDisc optimizations.
enum class projection_mode {
section_biggest_cell,
sector_biggest_cell,
sector_spanning_tree
};
// Tag indicating what kind of invariance we need to preserve for a polynomial on the cell:
// - SIGN: sign-invariance is sufficient (polynomial doesn't change sign within the cell)
// - ORD: order-invariance is required (root multiplicities are constant within the cell)
//
// Order-invariance is stronger than sign-invariance and is needed for:
// - Discriminants: to ensure root functions remain continuous and ordered (Theorem 2.1 in [1])
// - Resultants: to ensure relative ordering of roots from different polynomials (Theorem 2.2 in [1])
// Sign-invariance suffices for leading coefficients (ensures polynomial degree doesn't drop).
//
// [1] Nalbach et al., "Projective Delineability for Single Cell Construction", SC² 2025
enum class inv_req : uint8_t { none = 0, sign = 1, ord = 2 };
static inline inv_req max_req(inv_req a, inv_req b) {
return static_cast<uint8_t>(a) < static_cast<uint8_t>(b) ? b : a;
}
struct nullified_poly_exception {};
struct levelwise::impl {
solver& m_solver;
polynomial_ref_vector const& m_P;
unsigned m_n; // maximal variable we project from
pmanager& m_pm;
anum_manager& m_am;
polynomial::cache& m_cache;
std_vector<root_function_interval> m_I; // intervals for variables 0..m_n-1
unsigned m_level = 0; // current level being processed
unsigned m_spanning_tree_threshold = 3; // minimum both-side count for spanning tree
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
// Per-level state set by process_level/process_top_level
todo_set m_todo;
polynomial_ref_vector m_level_ps;
std_vector<polynomial_ref> m_witnesses;
std_vector<bool> m_poly_has_roots;
polynomial_ref_vector m_psc_tmp; // scratch for PSC chains
bool m_fail = false;
// Current requirement tag for polynomials stored in the todo_set, keyed by pm.id(poly*).
// Entries are upgraded SIGN -> ORD as needed and cleared when a polynomial is extracted.
std_vector<uint8_t> m_req;
// Vectors indexed by position in m_level_ps (more cache-friendly than maps)
mutable std_vector<uint8_t> m_side_mask; // bit0 = lower, bit1 = upper, 3 = both
mutable std_vector<bool> m_omit_lc;
mutable std_vector<bool> m_omit_disc;
mutable std_vector<unsigned> m_deg_in_order_graph; // degree of polynomial in resultant graph
mutable std_vector<unsigned> m_unique_neighbor; // UINT_MAX = not set, UINT_MAX-1 = multiple
assignment const& sample() const { return m_solver.sample(); }
// Utility: call fn for each distinct irreducible factor of poly
template <typename Func>
void for_each_distinct_factor(polynomial_ref const& poly_in, Func&& fn) {
if (!poly_in || is_zero(poly_in) || is_const(poly_in))
return;
polynomial_ref poly(poly_in);
polynomial_ref_vector factors(m_pm);
::nlsat::factor(poly, m_cache, factors);
for (unsigned i = 0; i < factors.size(); ++i) {
polynomial_ref f(factors.get(i), m_pm);
if (!f || is_zero(f) || is_const(f))
continue;
fn(f);
}
}
struct root_function {
scoped_anum val;
indexed_root_expr ire;
unsigned ps_idx; // index in m_level_ps
root_function(anum_manager& am, poly* p, unsigned idx, anum const& v, unsigned ps_idx)
: val(am), ire{ p, idx }, ps_idx(ps_idx) { am.set(val, v); }
root_function(root_function&& other) noexcept : val(other.val.m()), ire(other.ire), ps_idx(other.ps_idx) { val = other.val; }
root_function(root_function const&) = delete;
root_function& operator=(root_function const&) = delete;
root_function& operator=(root_function&& other) noexcept {
val = other.val;
ire = other.ire;
ps_idx = other.ps_idx;
return *this;
}
};
// Root functions (Theta) and the chosen relation (≼) on a given level.
struct relation_E {
std_vector<root_function> m_rfunc; // Θ: root functions on current level
std::set<std::pair<unsigned, unsigned>> m_pairs; // ≼ relation as unique m_level_ps index pairs
bool empty() const { return m_rfunc.empty() && m_pairs.empty(); }
void clear() {
m_pairs.clear();
m_rfunc.clear();
}
// Add pair by rfunc indices - canonicalizes and de-duplicates using ps_idx
void add_pair(unsigned j, unsigned k) {
unsigned a = m_rfunc[j].ps_idx;
unsigned b = m_rfunc[k].ps_idx;
if (a == b) return;
if (a > b) std::swap(a, b);
m_pairs.insert({a, b});
}
};
relation_E m_rel;
// Check that the relation forms a valid connected structure for order-invariance.
// Every root function on each side must be connected to the boundary through edges.
bool relation_invariant() const {
auto const& rfs = m_rel.m_rfunc;
unsigned n = rfs.size();
if (n == 0) return true;
// Build adjacency list from pairs (using ps_idx)
// Use vectors indexed by ps_idx for efficiency
unsigned max_ps_idx = 0;
for (auto const& rf : rfs)
if (rf.ps_idx > max_ps_idx) max_ps_idx = rf.ps_idx;
for (auto const& pr : m_rel.m_pairs) {
if (pr.first > max_ps_idx) max_ps_idx = pr.first;
if (pr.second > max_ps_idx) max_ps_idx = pr.second;
}
std_vector<std_vector<unsigned>> adj(max_ps_idx + 1);
for (auto const& pr : m_rel.m_pairs) {
adj[pr.first].push_back(pr.second);
adj[pr.second].push_back(pr.first);
}
// BFS to find all ps_idx reachable from a starting ps_idx
auto reachable_from = [&](unsigned start_ps_idx) -> std_vector<bool> {
std_vector<bool> visited(max_ps_idx + 1, false);
std_vector<unsigned> queue;
queue.push_back(start_ps_idx);
visited[start_ps_idx] = true;
while (!queue.empty()) {
unsigned curr = queue.back();
queue.pop_back();
for (unsigned neighbor : adj[curr]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.push_back(neighbor);
}
}
}
return visited;
};
// Check lower side: all rfuncs in [0, m_l_rf] must be connected to boundary
if (is_set(m_l_rf) && m_l_rf < n) {
unsigned boundary_ps_idx = rfs[m_l_rf].ps_idx;
std_vector<bool> reachable = reachable_from(boundary_ps_idx);
for (unsigned i = 0; i <= m_l_rf; ++i) {
unsigned ps_idx = rfs[i].ps_idx;
if (!reachable[ps_idx]) {
TRACE(lws, tout << "INVARIANT FAIL: lower side rfunc " << i
<< " (ps_idx=" << ps_idx << ") not connected to boundary "
<< m_l_rf << " (ps_idx=" << boundary_ps_idx << ")\n";);
return false;
}
}
}
// Check upper side: all rfuncs in [m_u_rf, n-1] must be connected to boundary
if (is_set(m_u_rf) && m_u_rf < n) {
unsigned boundary_ps_idx = rfs[m_u_rf].ps_idx;
std_vector<bool> reachable = reachable_from(boundary_ps_idx);
for (unsigned i = m_u_rf; i < n; ++i) {
unsigned ps_idx = rfs[i].ps_idx;
if (!reachable[ps_idx]) {
TRACE(lws, tout << "INVARIANT FAIL: upper side rfunc " << i
<< " (ps_idx=" << ps_idx << ") not connected to boundary "
<< m_u_rf << " (ps_idx=" << boundary_ps_idx << ")\n";);
return false;
}
}
}
return true;
}
// Weight function for estimating projection complexity.
// w(p, level) estimates the "cost" of polynomial p at the given level.
// w(disc(a)) ≈ 2*w(a), w(res(a,b)) ≈ w(a) + w(b)
unsigned w(poly* p, unsigned level) const {
if (!p) return 0;
return m_pm.degree(p, level);
}
// Estimate resultant weight for m_rel.m_pairs (pairs of m_level_ps indices)
unsigned estimate_resultant_weight() const {
unsigned total = 0;
for (auto const& pr : m_rel.m_pairs)
total += w(m_level_ps.get(pr.first), m_level) + w(m_level_ps.get(pr.second), m_level);
return total;
}
// Estimate leading coefficient weight for polynomials where omit_lc is false
unsigned estimate_lc_weight() const {
unsigned total = 0;
for (unsigned i = 0; i < m_level_ps.size(); ++i) {
if (vec_get(m_omit_lc, i, false))
continue;
poly* p = m_level_ps.get(i);
unsigned deg = m_pm.degree(p, m_level);
if (deg > 0)
total += w(m_pm.coeff(p, m_level, deg), m_level - 1);
}
return total;
}
// Choose the best sector heuristic based on estimated weight.
// Also fills m_rel.m_pairs with the winning heuristic's pairs.
// Note: spanning_tree is handled at a higher level (process_level_sector)
impl(
solver& solver,
polynomial_ref_vector const& ps,
var max_x,
assignment const&,
pmanager& pm,
anum_manager& am,
polynomial::cache& cache)
: m_solver(solver),
m_P(ps),
m_n(max_x),
m_pm(pm),
m_am(am),
m_cache(cache),
m_todo(m_cache, true),
m_level_ps(m_pm),
m_psc_tmp(m_pm) {
m_I.reserve(m_n);
for (unsigned i = 0; i < m_n; ++i)
m_I.emplace_back(m_pm);
m_spanning_tree_threshold = m_solver.lws_spt_threshold();
}
void fail() { throw nullified_poly_exception(); }
static void reset_interval(root_function_interval& I) {
I.section = false;
I.l = nullptr;
I.u = nullptr;
I.l_index = 0;
I.u_index = 0;
}
// PSC-based discriminant: returns the first non-zero, non-constant element from the PSC chain.
polynomial_ref psc_discriminant(polynomial_ref const& p_in, unsigned x) {
if (!p_in || is_zero(p_in) || is_const(p_in))
return polynomial_ref(m_pm);
if (m_pm.degree(p_in, x) < 2)
return polynomial_ref(m_pm);
polynomial_ref p(p_in);
polynomial_ref d = derivative(p, x);
polynomial_ref_vector& chain = m_psc_tmp;
chain.reset();
m_cache.psc_chain(p, d, x, chain);
polynomial_ref disc(m_pm);
for (unsigned i = 0; i < chain.size(); ++i) {
disc = polynomial_ref(chain.get(i), m_pm);
if (!disc || is_zero(disc))
continue;
if (is_const(disc))
return polynomial_ref(m_pm); // constant means discriminant is non-zero constant
return disc;
}
return polynomial_ref(m_pm);
}
// PSC-based resultant: returns the first non-zero, non-constant element from the PSC chain.
polynomial_ref psc_resultant(poly* a, poly* b, unsigned x) {
if (!a || !b)
return polynomial_ref(m_pm);
polynomial_ref pa(a, m_pm);
polynomial_ref pb(b, m_pm);
polynomial_ref_vector& chain = m_psc_tmp;
chain.reset();
m_cache.psc_chain(pa, pb, x, chain);
polynomial_ref r(m_pm);
// Iterate forward: S[0] is the resultant (after reverse in psc_chain)
for (unsigned i = 0; i < chain.size(); ++i) {
r = polynomial_ref(chain.get(i), m_pm);
if (!r || is_zero(r))
continue;
if (is_const(r))
return polynomial_ref(m_pm); // constant means polynomials are coprime
return r;
}
return polynomial_ref(m_pm);
}
poly* request(poly* p, inv_req req) {
if (!p || req == inv_req::none)
return p;
p = m_todo.insert(p);
unsigned id = m_pm.id(p);
auto cur = static_cast<inv_req>(vec_get(m_req, id, static_cast<uint8_t>(inv_req::none)));
inv_req nxt = max_req(cur, req);
if (nxt != cur)
vec_setx(m_req, id, static_cast<uint8_t>(nxt), static_cast<uint8_t>(inv_req::none));
return p;
}
void request_factorized(polynomial_ref const& poly, inv_req req) {
for_each_distinct_factor(poly, [&](polynomial_ref const& f) {
TRACE(lws,
m_pm.display(tout << " request_factorized: factor=", f) << " at level " << m_pm.max_var(f) << "\n";
);
// Each irreducible factor inherits the invariance requirement.
// If already requested with a weaker tag, upgrade to the stronger one.
request(f.get(), req);
});
}
// Extract polynomials at max level from m_todo into m_level_ps.
// Sets m_level to the extracted level. Requirements remain in m_req.
void extract_max_tagged() {
m_level = m_todo.extract_max_polys(m_level_ps);
// Ensure all extracted polynomials have at least SIGN requirement
for (unsigned i = 0; i < m_level_ps.size(); ++i) {
unsigned id = m_pm.id(m_level_ps.get(i));
if (vec_get(m_req, id, static_cast<uint8_t>(inv_req::none)) == static_cast<uint8_t>(inv_req::none))
vec_setx(m_req, id, static_cast<uint8_t>(inv_req::sign), static_cast<uint8_t>(inv_req::none));
}
}
// Get the requirement for polynomial at index i in m_level_ps
inv_req get_req(unsigned i) const {
unsigned id = m_pm.id(m_level_ps.get(i));
return static_cast<inv_req>(vec_get(m_req, id, static_cast<uint8_t>(inv_req::sign)));
}
// Set the requirement for polynomial at index i in m_level_ps
void set_req(unsigned i, inv_req req) {
unsigned id = m_pm.id(m_level_ps.get(i));
vec_setx(m_req, id, static_cast<uint8_t>(req), static_cast<uint8_t>(inv_req::none));
}
// Select a coefficient c of p (wrt x) such that c(s) != 0, or return null.
// The coefficients are polynomials in lower variables; we prefer "simpler" ones
// to reduce projection blow-up.
polynomial_ref choose_nonzero_coeff(polynomial_ref const& p, unsigned x) {
unsigned deg = m_pm.degree(p, x);
polynomial_ref coeff(m_pm);
// First pass: any non-zero constant coefficient is ideal (no need to project it).
for (unsigned j = 0; j <= deg; ++j) {
coeff = m_pm.coeff(p, x, j);
if (!coeff || is_zero(coeff))
continue;
if (is_const(coeff))
return coeff;
}
// Second pass: pick the non-constant coefficient with minimal (total_degree, size, max_var).
polynomial_ref best(m_pm);
unsigned best_td = 0;
unsigned best_sz = 0;
var best_mv = null_var;
for (unsigned j = 0; j <= deg; ++j) {
coeff = m_pm.coeff(p, x, j);
if (!coeff || is_zero(coeff))
continue;
if (m_am.eval_sign_at(coeff, sample()) == 0)
continue;
unsigned td = total_degree(coeff);
unsigned sz = m_pm.size(coeff);
var mv = m_pm.max_var(coeff);
if (!best ||
td < best_td ||
(td == best_td && (sz < best_sz ||
(sz == best_sz && (best_mv == null_var || mv < best_mv))))) {
best = coeff;
best_td = td;
best_sz = sz;
best_mv = mv;
}
}
return best;
}
void add_projection_for_poly(polynomial_ref const& p, unsigned x, polynomial_ref const& nonzero_coeff, bool add_leading_coeff, bool add_discriminant) {
TRACE(lws,
m_pm.display(tout << " add_projection_for_poly: p=", p)
<< " x=" << x << " add_lc=" << add_leading_coeff << " add_disc=" << add_discriminant << "\n";
);
// Line 11 (non-null witness coefficient)
if (nonzero_coeff && !is_const(nonzero_coeff))
request_factorized(nonzero_coeff, inv_req::sign);
// Line 12 (disc + leading coefficient)
if (add_discriminant)
request_factorized(psc_discriminant(p, x), inv_req::ord);
if (add_leading_coeff) {
unsigned deg = m_pm.degree(p, x);
polynomial_ref lc(m_pm);
lc = m_pm.coeff(p, x, deg);
TRACE(lws, m_pm.display(tout << " adding lc: ", lc) << "\n";);
request_factorized(lc, inv_req::sign);
}
}
// ============================================================================
// noLdcf optimization - compute which leading coefficients can be omitted
//
// This optimization is justified by PROJECTIVE DELINEABILITY theory [1,2].
//
// Regular delineability (Theorem 2.1 in [2]) requires the leading coefficient
// to be sign-invariant to ensure the polynomial's degree doesn't drop. However,
// projective delineability (Theorem 3.1 in [2]) shows that order-invariance of
// the discriminant alone (without LC sign-invariance) is sufficient to ensure
// the polynomial's roots behave continuously - they may "go to infinity" when
// the LC vanishes, but won't suddenly appear or disappear within the cell.
//
// For a polynomial p with roots on BOTH sides of the sample:
// - Resultants with both boundary polynomials are computed anyway
// - These resultants ensure p's roots don't cross the cell boundaries (Thm 3.2)
// - Even if p's degree drops (LC becomes zero), existing roots remain ordered
// - New roots can only appear "at infinity", outside the bounded cell
//
// [1] Michel et al., "On Projective Delineability", arXiv:2411.13300, 2024
// [2] Nalbach et al., "Projective Delineability for Single Cell Construction", SC² 2025
// ============================================================================
// Compute side_mask: track which side(s) each polynomial appears on
// bit0 = lower (<= sample), bit1 = upper (> sample), 3 = both sides
void compute_side_mask() {
if (!is_set(m_l_rf))
return;
m_side_mask.resize(m_level_ps.size(), 0);
for (unsigned i = 0; i < m_rel.m_rfunc.size(); i ++) {
unsigned ps_idx = m_rel.m_rfunc[i].ps_idx;
if (i <= m_l_rf)
m_side_mask[ps_idx] |= 1;
else
m_side_mask[ps_idx] |= 2;
}
}
// Check if lower and upper bounds come from the same polynomial
bool same_boundary_poly() const {
if (!is_set(m_l_rf) || !is_set(m_u_rf))
return false;
return m_rel.m_rfunc[m_l_rf].ps_idx == m_rel.m_rfunc[m_u_rf].ps_idx;
}
// Get lower bound polynomial index, or UINT_MAX if not set
unsigned lower_bound_idx() const {
return is_set(m_l_rf) ? m_rel.m_rfunc[m_l_rf].ps_idx : UINT_MAX;
}
// Get upper bound polynomial index, or UINT_MAX if not set
unsigned upper_bound_idx() const {
return is_set(m_u_rf) ? m_rel.m_rfunc[m_u_rf].ps_idx : UINT_MAX;
}
// Compute deg: count distinct resultant-neighbors for each polynomial
// m_pairs contains index pairs into m_level_ps
void compute_resultant_graph_degree() {
m_deg_in_order_graph.clear();
m_deg_in_order_graph.resize(m_level_ps.size(), 0);
m_unique_neighbor.clear();
m_unique_neighbor.resize(m_level_ps.size(), UINT_MAX);
for (auto const& pr : m_rel.m_pairs) {
unsigned idx1 = pr.first;
unsigned idx2 = pr.second;
m_deg_in_order_graph[idx1]++;
m_deg_in_order_graph[idx2]++;
// Track unique neighbor
auto update_neighbor = [&](unsigned idx, unsigned other) {
if (m_unique_neighbor[idx] == UINT_MAX)
m_unique_neighbor[idx] = other;
else if (m_unique_neighbor[idx] != other)
m_unique_neighbor[idx] = UINT_MAX - 1; // multiple neighbors
};
update_neighbor(idx1, idx2);
update_neighbor(idx2, idx1);
}
}
// TODO: Investigate noDisc optimization for spanning_tree mode when lb and ub are the same
// polynomial. SMT-RAT does noDisc for leaves connected to the section-defining polynomial
// in section case, which might extend to sectors where lb == ub. The previous implementation
// here was unsound because it applied to general leaves connected to any boundary, but
// discriminants are needed to ensure a polynomial's own roots don't merge/split.
// ----------------------------------------------------------------------------
// noLdcf heuristic helpers
// ----------------------------------------------------------------------------
// Compute noLdcf: which leading coefficients can be omitted using projective delineability.
//
// A polynomial p with roots on BOTH sides of the sample has resultants with both bounds.
// By projective delineability (Theorem 3.1 in [2]), we only need the discriminant to be
// order-invariant - the LC can be omitted because roots "going to infinity" don't affect
// sign-invariance within the bounded cell.
//
// - biggest_cell mode, require_leaf=false: all non-bound polys with both-side roots
// - spanning_tree mode, require_leaf=true: only leaves with deg == 1 and both-side roots
//
// [2] Nalbach et al., "Projective Delineability for Single Cell Construction", SC² 2025
void compute_omit_lc_both_sides(bool require_leaf) {
m_omit_lc.clear();
m_omit_lc.resize(m_level_ps.size(), false);
if (m_rel.m_rfunc.empty() || m_rel.m_pairs.empty() || m_side_mask.empty())
return;
if (require_leaf)
compute_resultant_graph_degree();
unsigned l_bound_idx = lower_bound_idx();
unsigned u_bound_idx = upper_bound_idx();
for (unsigned i = 0; i < m_level_ps.size(); ++i) {
if (m_side_mask[i] != 3)
continue;
if (require_leaf && m_deg_in_order_graph[i] != 1)
continue;
if (i == l_bound_idx || i == u_bound_idx)
continue;
m_omit_lc[i] = true;
}
}
// Relation construction heuristics (same intent as previous implementation).
void fill_relation_sector_biggest_cell() {
TRACE(lws,
tout << " fill_biggest_cell: m_l_rf=" << m_l_rf << ", m_u_rf=" << m_u_rf << ", rfunc.size=" << m_rel.m_rfunc.size() << "\n";
);
if (is_set(m_l_rf))
for (unsigned j = 0; j < m_l_rf; ++j) {
TRACE(lws, tout << " add_pair(" << j << ", " << m_l_rf << ")\n";);
m_rel.add_pair(j, m_l_rf);
}
if (is_set(m_u_rf))
for (unsigned j = m_u_rf + 1; j < m_rel.m_rfunc.size(); ++j) {
TRACE(lws, tout << " add_pair(" << m_u_rf << ", " << j << ")\n";);
m_rel.add_pair(m_u_rf, j);
}
if (is_set(m_l_rf) && is_set(m_u_rf)) {
SASSERT(m_l_rf + 1 == m_u_rf);
TRACE(lws, tout << " add_pair(" << m_l_rf << ", " << m_u_rf << ")\n";);
m_rel.add_pair(m_l_rf, m_u_rf);
}
TRACE(lws,
tout << " fill_biggest_cell done: pairs.size=" << m_rel.m_pairs.size() << "\n";
);
}
void fill_relation_pairs_for_section_biggest_cell() {
auto const& rfs = m_rel.m_rfunc;
unsigned n = rfs.size();
if (n == 0)
return;
SASSERT(is_set(m_l_rf));
SASSERT(m_l_rf < n);
for (unsigned j = 0; j < m_l_rf; ++j)
m_rel.add_pair(j, m_l_rf);
for (unsigned j = m_l_rf + 1; j < n; ++j)
m_rel.add_pair(m_l_rf, j);
}
// ============================================================================
// Spanning tree heuristic based on the Reaching Orders Lemma.
// For polynomials appearing on both sides of the sample, we build a spanning
// tree that ensures order-invariance on both sides with n-1 edges.
// ============================================================================
// Build spanning tree on both-side polynomials using the lemma construction.
// Adds pairs directly to m_rel.m_pairs. Returns true if tree was built.
// K = lower rfunc positions, f = upper rfunc positions
void build_both_side_spanning_tree() {
auto const& rfs = m_rel.m_rfunc;
unsigned n = rfs.size();
SASSERT(n > 0 && is_set(m_l_rf) && is_set(m_u_rf));
SASSERT(!is_section());
SASSERT(!same_boundary_poly());
// Collect both-side polynomials with their rfunc indices on each side
struct both_info {
unsigned ps_idx;
unsigned lower_rf; // rfunc index on lower side
unsigned upper_rf; // rfunc index on upper side
};
std_vector<both_info> both;
// For sector: lower side is [0, m_l_rf], upper side is [m_u_rf, n)
// Build map from ps_idx to rfunc indices
std_vector<unsigned> lower_rf(m_level_ps.size(), UINT_MAX);
std_vector<unsigned> upper_rf(m_level_ps.size(), UINT_MAX);
for (unsigned i = 0; i <= m_l_rf; ++i)
lower_rf[rfs[i].ps_idx] = i;
for (unsigned i = m_u_rf; i < n; ++i)
upper_rf[rfs[i].ps_idx] = i;
// Collect both-side polynomials
for (unsigned i = 0; i < m_level_ps.size(); ++i) {
if (m_side_mask[i] == 3) {
SASSERT(lower_rf[i] != UINT_MAX && upper_rf[i] != UINT_MAX);
both.push_back({i, lower_rf[i], upper_rf[i]});
}
}
SASSERT(both.size() >= m_spanning_tree_threshold);
// Sort by lower_rf (root position on lower side)
std::sort(both.begin(), both.end(),
[](both_info const& a, both_info const& b) { return a.lower_rf < b.lower_rf; });
// Build spanning tree using Reaching Orders Lemma (from the paper):
// Process elements in order of increasing lower_rf.
// For each new element a = min(remaining), connect to c where f(c) = min(f(K \ {a}))
// i.e., c has minimum upper_rf among all elements except a.
// Root of in-arborescence on lower side is max(K) = last element (max lower_rf)
// Root of out-arborescence on upper side is min(f(K)) = element with min upper_rf
unsigned upper_root_idx = 0;
for (unsigned i = 1; i < both.size(); ++i)
if (both[i].upper_rf < both[upper_root_idx].upper_rf)
upper_root_idx = i;
(void)upper_root_idx; // used in DEBUG_CODE below
// Process in order of lower_rf
// First element (index 0) has min lower_rf
for (unsigned a_idx = 0; a_idx < both.size() - 1; ++a_idx) {
// Find c = element with min upper_rf among all elements except a_idx
// Since we process in lower_rf order, elements [a_idx+1, n) are "K' = K \ {a}"
// and we need min upper_rf among them
unsigned c_idx = UINT_MAX;
for (unsigned i = a_idx + 1; i < both.size(); ++i)
if (c_idx == UINT_MAX || both[i].upper_rf < both[c_idx].upper_rf)
c_idx = i;
SASSERT(c_idx != UINT_MAX);
// Add edge {a, c}
unsigned ps_a = both[a_idx].ps_idx;
unsigned ps_c = both[c_idx].ps_idx;
m_rel.m_pairs.insert({std::min(ps_a, ps_c), std::max(ps_a, ps_c)});
}
// Check arborescence invariants (used in debug via SASSERT)
DEBUG_CODE(
unsigned lower_root_idx = both.size() - 1;
auto arb_invariant = [&]() {
// Reconstruct parent[] from the algorithm logic
std_vector<unsigned> parent(both.size(), UINT_MAX);
for (unsigned a_idx = 0; a_idx < both.size() - 1; ++a_idx) {
unsigned c_idx = UINT_MAX;
for (unsigned i = a_idx + 1; i < both.size(); ++i)
if (c_idx == UINT_MAX || both[i].upper_rf < both[c_idx].upper_rf)
c_idx = i;
parent[a_idx] = c_idx;
}
// Check it's a tree: exactly n-1 edges
unsigned edge_count = 0;
for (unsigned i = 0; i < both.size(); ++i)
if (parent[i] != UINT_MAX) edge_count++;
SASSERT(edge_count == both.size() - 1); // tree has n-1 edges
// Check roots are at extremes
// lower_root_idx should have max lower_rf
for (unsigned i = 0; i < both.size(); ++i)
SASSERT(both[i].lower_rf <= both[lower_root_idx].lower_rf);
// upper_root_idx should have min upper_rf
for (unsigned i = 0; i < both.size(); ++i)
SASSERT(both[i].upper_rf >= both[upper_root_idx].upper_rf);
// Root of in-arborescence (max lower_rf) has no parent
SASSERT(parent[lower_root_idx] == UINT_MAX);
for (unsigned i = 0; i < both.size(); ++i)
if (i != lower_root_idx)
SASSERT(parent[i] != UINT_MAX); // non-root has exactly 1 parent
// Check in-arborescence on left (lower) side:
// Each node can reach lower_root by following parent pointers
// and lower_rf increases (going right) at each step
for (unsigned i = 0; i < both.size(); ++i) {
unsigned curr = i;
unsigned steps = 0;
while (curr != lower_root_idx) {
unsigned p = parent[curr];
SASSERT(p != UINT_MAX);
// Going to parent increases lower_rf (toward root which has max lower_rf)
SASSERT(both[curr].lower_rf < both[p].lower_rf);
curr = p;
steps++;
SASSERT(steps < both.size()); // prevent infinite loop
}
}
// Check out-arborescence on right (upper) side:
// Re-orient edges based on upper_rf: edge goes from smaller to larger upper_rf
// Build adjacency from the edges in parent[]
std_vector<std_vector<unsigned>> out_children(both.size());
for (unsigned i = 0; i < both.size(); ++i) {
if (parent[i] != UINT_MAX) {
unsigned p = parent[i];
// Edge {i, p} exists. Orient based on upper_rf.
if (both[i].upper_rf < both[p].upper_rf)
out_children[i].push_back(p); // i has smaller upper_rf
else
out_children[p].push_back(i); // p has smaller upper_rf
}
}
// Each node can be reached from upper_root by following out_children
// and upper_rf increases (going away from root which has min upper_rf)
std_vector<bool> reached(both.size(), false);
std_vector<unsigned> stack;
stack.push_back(upper_root_idx);
reached[upper_root_idx] = true;
while (!stack.empty()) {
unsigned curr = stack.back();
stack.pop_back();
for (unsigned child : out_children[curr]) {
// Going to child increases upper_rf (away from root which has min upper_rf)
SASSERT(both[child].upper_rf > both[curr].upper_rf);
SASSERT(!reached[child]); // tree, no cycles
reached[child] = true;
stack.push_back(child);
}
}
// All nodes reachable from upper_root
for (unsigned i = 0; i < both.size(); ++i)
SASSERT(reached[i]);
return true;
};
SASSERT(arb_invariant()););
}
// Sector spanning tree heuristic:
// 1. Build spanning tree on both-side polynomials
// 2. Extend with lowest_degree for single-side polynomials
// Helper: Connect non-tree (single-side) polynomials to their respective boundaries
void connect_non_tree_to_bounds() {
auto const& rfs = m_rel.m_rfunc;
unsigned n = rfs.size();
// Lower side: connect single-side polys to lower boundary
for (unsigned j = 0; j < m_l_rf; ++j)
if (m_side_mask[rfs[j].ps_idx] != 3)
m_rel.add_pair(j, m_l_rf);
// Upper side: connect single-side polys to upper boundary
for (unsigned j = m_u_rf + 1; j < n; ++j)
if (m_side_mask[rfs[j].ps_idx] != 3)
m_rel.add_pair(m_u_rf, j);
}
// Helper: Connect spanning tree extremes to boundaries (when boundaries are different polys)
void connect_tree_extremes_to_bounds() {
auto const& rfs = m_rel.m_rfunc;
unsigned n = rfs.size();
// Find max lower both-side poly (closest to lower boundary from below)
unsigned max_lower_both = UINT_MAX;
for (unsigned j = 0; j < m_l_rf; ++j)
if (m_side_mask[rfs[j].ps_idx] == 3)
max_lower_both = j;
// Find min upper both-side poly (closest to upper boundary from above)
unsigned min_upper_both = UINT_MAX;
for (unsigned j = m_u_rf + 1; j < n; ++j) {
if (m_side_mask[rfs[j].ps_idx] == 3) {
min_upper_both = j;
break;
}
}
// Connect tree extremes to boundaries
if (max_lower_both != UINT_MAX)
m_rel.add_pair(max_lower_both, m_l_rf);
if (min_upper_both != UINT_MAX)
m_rel.add_pair(m_u_rf, min_upper_both);
}
void fill_relation_sector_spanning_tree() {
build_both_side_spanning_tree();
connect_non_tree_to_bounds();
connect_tree_extremes_to_bounds(); // otherwise the trees on both sides are connected to bounds already
// Connect lower and upper boundaries
SASSERT(m_l_rf + 1 == m_u_rf);
m_rel.add_pair(m_l_rf, m_u_rf);
}
// Extract roots of polynomial p around sample value v at the given level,
// partitioning them into lhalf (roots <= v) and uhalf (roots > v).
// ps_idx is the index of p in m_level_ps.
// Returns whether the polynomial has any roots.
bool isolate_roots_around_sample(poly* p, unsigned ps_idx,
anum const& v, std_vector<root_function>& lhalf,
std_vector<root_function>& uhalf) {
scoped_anum_vector roots(m_am);
// When the sample value is rational use a closest-root isolation
// returning at most closest to the sample roots
if (v.is_basic()) {
scoped_mpq s(m_am.qm());
m_am.to_rational(v, s);
svector<unsigned> idx_vec;
m_am.isolate_roots_closest(polynomial_ref(p, m_pm), undef_var_assignment(sample(), m_level), s, roots, idx_vec);
SASSERT(roots.size() == idx_vec.size());
SASSERT(roots.size() < 2 ||(roots.size() == 2 && m_am.compare(roots[0], v) < 0 && m_am.compare(roots[1], v) > 0));
for (unsigned k = 0; k < roots.size(); ++k) {
if (m_am.compare(roots[k], v) <= 0)
lhalf.emplace_back(m_am, p, idx_vec[k], roots[k], ps_idx);
else
uhalf.emplace_back(m_am, p, idx_vec[k], roots[k], ps_idx);
}
return roots.size();
}
m_am.isolate_roots(polynomial_ref(p, m_pm), undef_var_assignment(sample(), m_level), roots);
// For single cell construction, we only need the closest roots to the sample point:
// at most one root below v and one root above v, or a single root at v for sections.
// Other roots are irrelevant for bounding the cell containing the sample.
unsigned lower = UINT_MAX, upper = UINT_MAX;
bool section = false;
for (unsigned k = 0; k < roots.size(); ++k) {
int cmp = m_am.compare(roots[k], v);
if (cmp <= 0) {
lower = k;
if (cmp == 0) {
section = true;
break;
}
}
else {
upper = k;
break;
}
}
if (lower != UINT_MAX) {
lhalf.emplace_back(m_am, p, lower + 1, roots[lower], ps_idx);
if (section)
return roots.size(); // only keep the section root for this polynomial
}
if (upper != UINT_MAX)
uhalf.emplace_back(m_am, p, upper + 1, roots[upper], ps_idx);
return roots.size();
}
void init_poly_has_roots() {
m_poly_has_roots.clear();
m_poly_has_roots.resize(m_level_ps.size(), false);
}
bool collect_partitioned_root_functions_around_sample(anum const& v,
std_vector<root_function>& lhalf, std_vector<root_function>& uhalf) {
for (unsigned i = 0; i < m_level_ps.size(); ++i)
m_poly_has_roots[i] = isolate_roots_around_sample(m_level_ps.get(i), i, v, lhalf, uhalf);
return !lhalf.empty() || !uhalf.empty();
}
std_vector<root_function>::iterator set_relation_root_functions_from_partitions(
std_vector<root_function>& lhalf,
std_vector<root_function>& uhalf) {
auto& rfs = m_rel.m_rfunc;
size_t mid_pos = lhalf.size();
rfs.clear();
rfs.reserve(mid_pos + uhalf.size());
for (auto& rf : lhalf)
rfs.emplace_back(std::move(rf));
for (auto& rf : uhalf)
rfs.emplace_back(std::move(rf));
return rfs.begin() + mid_pos;
}
bool root_function_lt(root_function const& a, root_function const& b, bool degree_desc) {
if (a.ire.p == b.ire.p)
return a.ire.i < b.ire.i;
auto r = m_am.compare(a.val, b.val);
if (r)
return r == sign_neg;
unsigned dega = m_pm.degree(a.ire.p, m_level);
unsigned degb = m_pm.degree(b.ire.p, m_level);
if (dega != degb)
return degree_desc ? dega > degb : dega < degb;
return m_pm.id(a.ire.p) < m_pm.id(b.ire.p);
}
void sort_root_function_partitions(std_vector<root_function>::iterator mid) {
auto& rfs = m_rel.m_rfunc;
std::sort(rfs.begin(), mid,
[&](root_function const& a, root_function const& b) { return root_function_lt(a, b, true); });
std::sort(mid, rfs.end(),
[&](root_function const& a, root_function const& b) { return root_function_lt(a, b, false); });
}
// Populate Θ (root functions) around the sample, partitioned at `mid`, and sort each partition.
// Returns whether any roots were found.
bool build_sorted_root_functions_around_sample(anum const& v, std_vector<root_function>::iterator& mid) {
init_poly_has_roots();
std_vector<root_function> lhalf, uhalf;
if (!collect_partitioned_root_functions_around_sample(v, lhalf, uhalf))
return false;
mid = set_relation_root_functions_from_partitions(lhalf, uhalf);
// Sort each partition separately. For ties (equal root values), prefer low-degree defining
// polynomials as interval boundaries:
// - lower bound comes from the last element in the <= partition, so sort ties by degree descending
// - upper bound comes from the first element in the > partition, so sort ties by degree ascending
sort_root_function_partitions(mid);
return true;
}
// Pick I_level around sample(m_level) from sorted Θ, split at `mid`.
// Sets m_l_rf/m_u_rf (positions in m_rfunc) and m_I[m_level] (interval with root indices).
void set_interval_from_root_partition(anum const& v, std_vector<root_function>::iterator mid) {
auto& rfs = m_rel.m_rfunc;
if (mid != rfs.begin()) {
m_l_rf = static_cast<unsigned>((mid - rfs.begin()) - 1);
auto& r = *(mid - 1);
if (m_am.eq(r.val, v)) {
// Section case: only m_l_rf is defined
m_I[m_level].section = true;
m_I[m_level].l = r.ire.p;
m_I[m_level].l_index = r.ire.i;
m_u_rf = m_l_rf;
}
else {
m_I[m_level].l = r.ire.p;
m_I[m_level].l_index = r.ire.i;
if (mid != rfs.end()) {
m_u_rf = m_l_rf + 1;
m_I[m_level].u = mid->ire.p;
m_I[m_level].u_index = mid->ire.i;
}
}
}
else {
// sample is below all roots: I = (-oo, theta_1)
m_l_rf = UINT_MAX;
m_u_rf = 0;
auto& r = *mid;
m_I[m_level].u = r.ire.p;
m_I[m_level].u_index = r.ire.i;
}
}
// Build Θ (root functions) and pick I_level around sample(level).
// Sets m_l_rf/m_u_rf and m_I[level].
// Returns whether any roots were found (i.e., whether a relation can be built).
bool build_interval() {
m_rel.clear();
reset_interval(m_I[m_level]);
m_l_rf = UINT_MAX;
m_u_rf = UINT_MAX;
anum const& v = sample().value(m_level);
std_vector<root_function>::iterator mid;
if (!build_sorted_root_functions_around_sample(v, mid))
return false;
set_interval_from_root_partition(v, mid);
compute_side_mask();
return true;
}
void add_relation_resultants() {
TRACE(lws, tout << " add_relation_resultants: " << m_rel.m_pairs.size() << " pairs\n";);
for (auto const& pr : m_rel.m_pairs) {
poly* p1 = m_level_ps.get(pr.first);
poly* p2 = m_level_ps.get(pr.second);
TRACE(lws,
m_pm.display(m_pm.display(tout << " resultant(" << pr.first << ", " << pr.second << "): ", p1) << " and ", p2) << "\n";
);
polynomial_ref res = psc_resultant(p1, p2, m_level);
TRACE(lws,
tout << " resultant poly: ";
if (res)
m_pm.display(tout, res) << "\n resultant sign at sample: " << m_am.eval_sign_at(res, sample());
else
tout << "(null)";
tout << "\n";
);
request_factorized(res, inv_req::ord);
}
}
// Top level (m_n): add resultants between adjacent roots of different polynomials.
// Fills m_poly_has_roots as a side effect.
void add_adjacent_root_resultants() {
m_poly_has_roots.clear();
m_poly_has_roots.resize(m_level_ps.size(), false);
std_vector<std::pair<scoped_anum, poly*>> root_vals;
for (unsigned i = 0; i < m_level_ps.size(); ++i) {
poly* p = m_level_ps.get(i);
scoped_anum_vector roots(m_am);
m_am.isolate_roots(polynomial_ref(p, m_pm), undef_var_assignment(sample(), m_n), roots);
m_poly_has_roots[i] = !roots.empty();
TRACE(lws,
tout << " poly[" << i << "] has " << roots.size() << " roots: ";
for (unsigned k = 0; k < roots.size(); ++k) {
if (k > 0) tout << ", ";
m_am.display_decimal(tout, roots[k], 5);
}
tout << "\n";
);
for (unsigned k = 0; k < roots.size(); ++k) {
scoped_anum root_v(m_am);
m_am.set(root_v, roots[k]);
root_vals.emplace_back(std::move(root_v), p);
}
}
if (root_vals.size() < 2)
return;
std::sort(root_vals.begin(), root_vals.end(), [&](auto const& a, auto const& b) {
return m_am.lt(a.first, b.first);
});
TRACE(lws,
tout << " Sorted roots:\n";
for (unsigned j = 0; j < root_vals.size(); ++j)
m_pm.display(m_am.display_decimal(tout << " [" << j << "] val=", root_vals[j].first, 5) << " poly=", root_vals[j].second) << "\n";
);
std::set<std::pair<poly*, poly*>> added_pairs;
for (unsigned j = 0; j + 1 < root_vals.size(); ++j) {
poly* p1 = root_vals[j].second;
poly* p2 = root_vals[j + 1].second;
if (!p1 || !p2 || p1 == p2)
continue;
if (p1 > p2) std::swap(p1, p2);
if (!added_pairs.insert({p1, p2}).second)
continue;
TRACE(lws,
m_pm.display(m_pm.display(tout << " Adjacent resultant pair: ", p1) << " and ", p2) << "\n";
);
request_factorized(psc_resultant(p1, p2, m_n), inv_req::ord);
}
}
void upgrade_bounds_to_ord() {
poly* lb = m_I[m_level].l;
poly* ub = m_I[m_level].u;
for (unsigned i = 0; i < m_level_ps.size(); ++i) {
poly* p = m_level_ps.get(i);
if (p == lb || p == ub)
set_req(i, inv_req::ord);
}
}
bool is_section() { return m_I[m_level].is_section();}
// Section case: the section-defining polynomial needs disc and lc projections.
// For polynomials WITH roots: resultants with section polynomial ensure root ordering.
// For polynomials WITHOUT roots: they need LC + disc to ensure they don't gain roots.
//
// Theory: For a section (sample on a root), sign-invariance of polynomials with roots
// is ensured by resultants with the section-defining polynomial (Thm 2.2 in [1]).
// But polynomials without roots need delineability (LC + disc) to stay root-free.
//
// [1] Nalbach et al., "Projective Delineability for Single Cell Construction", SC² 2025
void add_section_projections() {
poly* section_poly = m_I[m_level].l;
SASSERT(section_poly);
// Build set of polynomial indices that have roots at this level
std::set<unsigned> has_roots;
for (auto const& rf : m_rel.m_rfunc)
has_roots.insert(rf.ps_idx);
for (unsigned i = 0; i < m_level_ps.size(); ++i) {
polynomial_ref p(m_level_ps.get(i), m_pm);
polynomial_ref witness = m_witnesses[i];
if (m_level_ps.get(i) == section_poly)
add_projection_for_poly(p, m_level, witness, true, true); // section poly: full projection
else if (has_roots.find(i) == has_roots.end())
add_projection_for_poly(p, m_level, witness, true, true); // no roots: need LC+disc for delineability
else if (witness && !is_const(witness))
request_factorized(witness, inv_req::sign); // has roots: witness only
}
}
// Sector case: projection loop using m_omit_lc and m_omit_disc.
void add_sector_projections() {
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_level);
lc = m_pm.coeff(p, m_level, deg);
bool add_lc = !vec_get(m_omit_lc, i, false);
bool add_disc = !vec_get(m_omit_disc, i, false);
TRACE(lws,
tout << " poly[" << i << "]";
tout << " omit_lc=" << vec_get(m_omit_lc, i, false);
tout << " omit_disc=" << vec_get(m_omit_disc, i, false);
tout << "\n";
);
// coeffNonNull optimization: if the leading coefficient is already non-zero at the
// sample point AND we're projecting it, the LC itself serves as the non-null witness.
// 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_am.eval_sign_at(lc, sample()) != 0)
witness = polynomial_ref(m_pm);
add_projection_for_poly(p, m_level, witness, add_lc, add_disc);
}
}
// Check if spanning tree should be used based on both_count threshold.
// Note: This is only called from process_level_sector which handles non-section cases.
// Spanning tree requires distinct lower/upper bounds and enough both-side polys.
bool should_use_spanning_tree() {
SASSERT(!is_section()); // spanning_tree is for sector case only
// Need at least 2 polynomials for a spanning tree to have edges
if (m_spanning_tree_threshold < 2)
return false;
if (m_rel.m_rfunc.size() < 4) // 2 different bounds + at least 2 same out of bounds
return false;
if (m_side_mask.size() == 0)
return false;
if (!is_set(m_l_rf) || !is_set(m_u_rf))
return false;
if (same_boundary_poly())
return false; // spanning tree requires distinct lower/upper bounds
unsigned both_count = 0;
for (unsigned i = 0; i < m_level_ps.size(); ++i)
if (m_side_mask[i] == 3)
if (++both_count >= m_spanning_tree_threshold)
return true;
return false;
}
// Clear all per-level state that could be stale from previous levels.
// This ensures no leftover heuristic decisions affect the current level.
void clear_level_state() {
m_omit_lc.clear();
m_omit_disc.clear();
m_rel.m_pairs.clear();
m_side_mask.clear();
m_deg_in_order_graph.clear();
m_unique_neighbor.clear();
}
void process_level_with_mode(projection_mode mode, bool have_interval) {
if (have_interval) {
switch (mode) {
case projection_mode::section_biggest_cell:
fill_relation_pairs_for_section_biggest_cell();
break;
case projection_mode::sector_biggest_cell:
fill_relation_sector_biggest_cell();
compute_side_mask();
compute_omit_lc_both_sides(/*require_leaf=*/false);
// m_omit_disc stays empty - all discriminants added
break;
case projection_mode::sector_spanning_tree:
fill_relation_sector_spanning_tree();
compute_side_mask();
compute_omit_lc_both_sides(/*require_leaf=*/true);
// m_omit_disc stays empty - all discriminants added
break;
}
SASSERT(relation_invariant());
}
upgrade_bounds_to_ord();
if (mode == projection_mode::section_biggest_cell)
add_section_projections();
else
add_sector_projections();
add_relation_resultants();
}
void collect_non_null_witnesses() {
// Line 10/11: detect nullification + pick a non-zero coefficient witness per p.
m_witnesses.clear();
m_witnesses.reserve(m_level_ps.size());
for (unsigned i = 0; i < m_level_ps.size(); ++i) {
polynomial_ref p(m_level_ps.get(i), m_pm);
polynomial_ref w = choose_nonzero_coeff(p, m_level);
if (!w)
fail();
m_witnesses.push_back(w);
}
}
void display_polys_at_level(std::ostream& out) {
TRACE(lws, out << "Polynomials at level " << m_level << "\n";
for (unsigned i = 0; i < m_level_ps.size(); ++i)
m_pm.display(out, m_level_ps.get(i)) << "\n";);
}
void process_level() {
TRACE(lws, display_polys_at_level(tout););
clear_level_state(); // Clear stale state from previous level
collect_non_null_witnesses();
// Lines 3-8: Θ + I_level + relation ≼
bool have_interval = build_interval();
TRACE(lws,
display(tout << "Interval: ", m_solver, m_I[m_level])
<< "\nSection? " << (m_I[m_level].section ? "yes" : "no")
<< "\nhave_interval=" << have_interval << ", rfunc.size=" << m_rel.m_rfunc.size() << "\n";
for (unsigned i = 0; i < m_rel.m_rfunc.size(); ++i)
m_am.display(tout << " rfunc[" << i << "]: ps_idx=" << m_rel.m_rfunc[i].ps_idx << ", val=", m_rel.m_rfunc[i].val) << "\n";
);
projection_mode mode;
if (m_I[m_level].section)
mode = projection_mode::section_biggest_cell;
else if (should_use_spanning_tree())
mode = projection_mode::sector_spanning_tree;
else
mode = projection_mode::sector_biggest_cell;
process_level_with_mode(mode, have_interval);
}
bool poly_has_roots(unsigned i) { return vec_get(m_poly_has_roots, i, false); }
void process_top_level() {
TRACE(lws, display_polys_at_level(tout););
collect_non_null_witnesses();
add_adjacent_root_resultants();
// Projections (coeff witness, disc, leading coeff).
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);
bool add_lc = true;
if (!poly_has_roots(i))
if (lc && !is_zero(lc) && m_am.eval_sign_at(lc, sample()) != 0)
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) && m_am.eval_sign_at(lc, sample()) != 0)
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
}
}
std_vector<root_function_interval> single_cell_work() {
TRACE(lws,
tout << "Input polynomials (" << m_P.size() << "):\n";
for (unsigned i = 0; i < m_P.size(); ++i)
m_pm.display(tout << " p[" << i << "]: ", m_P.get(i)) << "\n";
tout << "Sample values:\n";
for (unsigned j = 0; j < m_n; ++j)
m_am.display(tout << " x" << j << " = ", sample().value(j)) << "\n";
);
if (m_n == 0) return m_I;
m_todo.reset();
// Initialize m_todo with distinct irreducible factors of the input polynomials.
for (unsigned i = 0; i < m_P.size(); ++i) {
polynomial_ref pi(m_P.get(i), m_pm);
for_each_distinct_factor(pi, [&](polynomial_ref const& f) {
request(f.get(), inv_req::sign);
});
}
if (m_todo.empty()) return m_I;
// Process top level m_n (we project from x_{m_n} and do not return an interval for it).
if (m_todo.max_var() == m_n) {
extract_max_tagged();
process_top_level();
}
// Now iteratively process remaining levels (descending).
while (!m_todo.empty()) {
extract_max_tagged();
SASSERT(m_level < m_n);
process_level();
TRACE(lws,
tout << "After level " << m_level << ": m_todo.empty()=" << m_todo.empty();
if (!m_todo.empty()) tout << ", m_todo.max_var()=" << m_todo.max_var();
tout << "\n";
);
}
TRACE(lws,
for (unsigned i = 0; i < m_I.size(); ++i)
display(tout << "I[" << i << "]: ", m_solver, m_I[i]) << "\n";
);
return m_I;
}
std_vector<root_function_interval> single_cell() {
try {
return single_cell_work();
}
catch (nullified_poly_exception&) {
m_fail = true;
return m_I;
}
}
};
levelwise::levelwise(
nlsat::solver& solver,
polynomial_ref_vector const& ps,
var n,
assignment const& s,
pmanager& pm,
anum_manager& am,
polynomial::cache& cache)
: m_impl(new impl(solver, ps, n, s, pm, am, cache)) {}
levelwise::~levelwise() { delete m_impl; }
std_vector<levelwise::root_function_interval> levelwise::single_cell() {
return m_impl->single_cell();
}
bool levelwise::failed() const { return m_impl->m_fail; }
} // namespace nlsat
// Free pretty-printer for symbolic_interval
std::ostream& nlsat::display(std::ostream& out, solver& s, levelwise::root_function_interval const& I) {
if (I.section) {
out << "Section: ";
if (I.l == nullptr)
out << "(undef)";
else {
::nlsat::display(out, s, I.l);
out << "[root_index:" << I.l_index << "]";
}
}
else {
out << "Sector: (";
if (I.l_inf())
out << "-oo";
else {
::nlsat::display(out, s, I.l);
out << "[root_index:" << I.l_index << "]";
}
out << ", ";
if (I.u_inf())
out << "+oo";
else {
::nlsat::display(out, s, I.u);
out << "[root_index:" << I.u_index << "]";
}
out << ")";
}
return out;
}