# Comparison Report: Case Splits in `smt_parallel.cpp` vs. AriParti ## Overview This report compares how **Z3's `src/smt/smt_parallel.cpp`** and **AriParti** (`src/partitioner/src/math/subpaving/subpaving_t_def.h` + orchestrated by `src/AriParti.py`) perform case splits in parallel arithmetic SMT solving. --- ## Architecture | Aspect | Z3 `smt_parallel.cpp` | AriParti | |---|---|---| | Parallelism model | Shared-memory multi-threaded (C++ threads, one `smt::context` per worker) | Process-based distributed (partitioner process + N solver sub-processes, IPC via named pipes) | | Where splits happen | Inside each SMT worker thread, using the live SMT context state | In a standalone subpaving partitioner process, before handing off to external solvers | | Split representation | `expr_ref` atom (e.g., `x ≤ mid`) passed to `batch_manager`, which builds a search tree | Bound pair (`x ≤ mid` / `x > mid`) installed on two child nodes in the subpaving tree | | Sub-problem dispatch | Workers pull cubes from a shared `search_tree` via `batch_manager::get_cube()` | Child nodes are serialised to `.smt2` files and dispatched to external solver processes | --- ## Case Split Flow ### Z3 (`smt_parallel.cpp`) ``` worker::run() → check_cube() [run SMT check with current cube] → l_undef result → get_split_atom() 1. get_arith_split_atom() ← AriParti-style arithmetic split 2. VSIDS Boolean fallback ← if no arithmetic atom found → batch_manager::split() ← insert atom into search tree (binary split) → simplify() ← optional inprocessing ``` ### AriParti (`subpaving_t_def.h` / `AriParti.py`) ``` context_t::operator()() ← main partitioner loop → create_new_task() ← if a leaf is ready, emit it as an .smt2 task → select_split_node() ← pick highest-priority leaf from heap → split_node(n) → select_best_var(n) ← multi-key heuristic → compute midpoint → mk_node(left), mk_node(right) → propagate(left/right) ← BICP immediately → emit "unsat-task" or "new-task" messages to master ``` --- ## Variable Selection Heuristic ### Z3 `get_arith_split_atom()` Iterates over all 0-arity arithmetic enodes visible in the current SMT context. For each variable it records: - `occ`: count of arithmetic-comparison parent enodes (≤, ≥, <, >, =) - `has_lo`, `has_up`, `lo`, `up`: current theory bounds from `arith_value` **Ranking (highest priority first):** 1. **Higher occurrence count** – more active in constraint network 2. **Fully bounded preferred over half-bounded or unbounded** 3. **Wider interval** (for fully-bounded variables) 4. **Interval contains zero** – as tiebreaker Variables whose interval is already a point (`lo == up`) are skipped. ### AriParti `select_best_var()` Uses a `var_info` struct with five keys and a *dynamic key ordering* (`key_rank`) that rotates per node to break ties diversely: | Key index | Criterion | Better is… | |---|---|---| | 0 | `m_split_cnt` – global split count | Lower (less-split variables preferred) | | 1 | `m_cz` – interval contains zero | `true` preferred | | 2 | `m_deg` – max polynomial degree | Higher preferred (prioritises nonlinear vars) | | 3 | `m_occ` – occurrence in undecided clauses | Higher preferred | | 4 | `m_width` – interval width (with penalties for unbounded) | Wider preferred | Key ordering for a child node is derived from its parent's `key_rank` by rotating one position whenever the parent's ranking had consecutive equal keys; this diversifies splits across the tree. Variables that are fully determined (`lower == upper`), Boolean (`m_is_bool`), or defined (`m_defs[x] != nullptr`) are excluded. **Unbounded width penalties (AriParti only):** - Fully unbounded: `width = 1024²` - Half-bounded (`(-∞, u]`): `width = 1024 + u` (if `u ≥ 0`), or `width = 1024 / max(1, -u)` (if `u < 0`) - Half-bounded (`[l, ∞)`): symmetric formula --- ## Midpoint Computation Both implementations agree on the core zero-split heuristic (AriParti Section 4.2): **if the interval straddles zero, split at 0**. | Case | Z3 `get_arith_split_atom()` | AriParti `split_node()` | |---|---|---| | Interval contains 0 | `mid = 0` | `mid = 0` | | Fully bounded `[lo, up]` | `mid = (lo + up) / 2`; `floor(mid)` for integers | `mid = (lo + up) / 2`; ceiling applied if `width > 10` | | Half-bounded `[lo, ∞)` | `mid = lo` (split at lower bound) | `mid = lo + delta` (`delta = 128`) | | Half-bounded `(-∞, up]` | `mid = up - 1` (int) or `up - 0.5` (real) | `mid = floor(up) - delta` | | Fully unbounded | `mid = 0` | `mid = 0` (via `m_cz = true`) | The split produces two branches: `x ≤ mid` and `x > mid` in both cases. --- ## Node / Leaf Selection ### Z3 Workers call `batch_manager::get_cube()`, which calls `search_tree::activate_node()` and `find_active_node()`. The search tree uses a simple work-stealing model: each worker tries to continue the node it last worked on; if unavailable, it picks any open node. There is no explicit priority between open nodes. ### AriParti Uses a **priority heap** (`m_leaf_heap`, ordered by `node_info::operator<`): 1. **Lowest depth first** – broader, shallower problems are split first 2. **Most undecided clauses** – more constrained nodes processed first 3. **Most undecided literals** 4. **Lowest node ID** (earlier-created nodes preferred as tiebreaker) This gives AriParti more control over the shape of the partitioning tree. --- ## Post-Split Processing | Aspect | Z3 | AriParti | |---|---|---| | Bound propagation after split | None immediately; full SMT check on next `check_cube()` | Immediate BICP (`propagate()`) on both children; inconsistent children are pruned at once | | Inprocessing simplification | Optional `simplify()` after split (configurable, one-shot) | None; each sub-task is a fresh SMT instance | | Learned clause sharing | Yes: units and clauses shared via `batch_manager::collect_clause()` | None: each solver instance is independent | | Backtracking | `batch_manager::backtrack()` annotates nodes in search tree with unsat-cores | Master (`AriParti.py`) propagates `unsat` upward and downward via `push_up` / `push_down` | --- ## Boolean Variables - **Z3**: When `get_arith_split_atom()` finds no arithmetic variable, it falls back to a VSIDS-style Boolean heuristic: selects the unassigned Boolean variable with the highest product of positive/negative literal activity scores (`m_lit_scores[0][v] * m_lit_scores[1][v]`), then halves both scores. - **AriParti**: `select_best_var()` explicitly skips Boolean variables (`if (m_is_bool[x]) continue`). Boolean case splits are not performed by the partitioner; they occur only inside each external solver instance. --- ## Shared Origins and Similarities Both implementations are directly inspired by the AriParti paper (Section 4.2): 1. **Zero-split heuristic**: both preferentially split at 0 when the interval straddles zero. 2. **Midpoint bisection**: both use `(lo + up) / 2` for fully-bounded intervals. 3. **Occurrence count**: both use the number of arithmetic constraints that mention a variable as a key factor. 4. **Interval width**: both prefer variables with wider intervals as a heuristic for "less constrained" variables. 5. **Binary tree structure**: both build a binary case-split tree where each split adds exactly two child nodes. 6. **Arithmetic-only primary heuristic**: both focus exclusively on arithmetic (Int/Real) variables for the primary split decision. The Z3 implementation (`get_arith_split_atom()`) was explicitly added to `smt_parallel.cpp` as an "AriParti-style arithmetic interval split" (see comment at line 543–549), making it a direct re-implementation of the core idea from the AriParti paper within Z3's parallel SMT framework. --- ## Key Differences Summary | Dimension | Z3 `smt_parallel.cpp` | AriParti | |---|---|---| | **Architecture** | Shared-memory threads inside Z3 | Distributed processes with IPC | | **Variable selection keys** | 3 (occ, width, cz) | 5 (split\_cnt, cz, deg, occ, width) with dynamic ordering | | **Split-count tracking** | Not tracked | Tracked globally; fewer-split vars preferred | | **Polynomial degree** | Not considered | Considered (key 2: higher degree preferred) | | **Boolean fallback** | VSIDS-style score-based | None (Boolean vars skipped entirely) | | **Unbounded width** | Sentinel `−1`; fully-bounded strictly preferred | Penalty values (1024, 1024²) enable ranking across all cases | | **Split delta (semi-unbounded)** | `1` (int) / `0.5` (real) | `128` (fixed `m_split_delta`) | | **Node priority** | Work-stealing, no explicit ordering | Priority heap (depth, undecided clauses, undecided literals) | | **Post-split propagation** | None until full SMT check | Immediate BICP; infeasible children pruned instantly | | **Clause learning across workers** | Yes (shared via batch\_manager) | No (each solver is isolated) | | **Inprocessing** | Optional (`m_inprocessing` flag) | None (whole new sub-task per child) | | **SLS portfolio** | Yes (optional parallel SLS worker) | No | --- ## Source Locations | Component | File | |---|---| | Z3 arithmetic split | `src/smt/smt_parallel.cpp`, `worker::get_arith_split_atom()` (lines 414–534) | | Z3 Boolean split fallback | `src/smt/smt_parallel.cpp`, `worker::get_split_atom()` (lines 537–570) | | Z3 search tree split | `src/smt/smt_parallel.cpp`, `batch_manager::split()` (lines 342–358) | | AriParti variable selection | `src/partitioner/src/math/subpaving/subpaving_t_def.h`, `select_best_var()` (lines 2354–2421) | | AriParti var\_info ranking | `src/partitioner/src/math/subpaving/subpaving_t.h`, `struct var_info`, `operator<` (lines 453–519) | | AriParti split\_node | `src/partitioner/src/math/subpaving/subpaving_t_def.h`, `split_node()` (lines 2516–2610) | | AriParti node priority | `src/partitioner/src/math/subpaving/subpaving_t.h`, `struct node_info`, `operator<` (lines 430–451) | | AriParti master orchestration | `src/AriParti.py`, `ArithPartition::solve()` and `push_up()` / `push_down()` |